Related: Deployment Overview · First Deploy · Billing & Cost
npm run localess:setup provisions everything Localess needs in a Firebase/GCP project. It
replaces the manual console walkthrough (project creation, Identity Platform, Firestore, Storage,
service accounts, API enablement) with one idempotent command.
npm install
npx firebase login # once per machine
npm run localess:setup -- --project my-localess # adopt an existing project
npm run localess:setup # or pick from a list / create a new oneEvery step is idempotent. If a run fails halfway, fix the cause and re-run — completed work is detected and skipped.
| Flag | Default | Purpose |
|---|---|---|
--project <id> |
— | Adopt an existing project. Omit to pick from a list of your projects. |
--display-name <name> |
Localess |
Display name when creating a project. |
--region <region> |
existing Firestore location, else asked (europe-west6 under --yes) |
Region for Firestore, Storage and Cloud Functions. Partly immutable — see below. |
--billing-account <id> |
— | Billing account to link. Required with --yes when billing is not yet enabled. |
--yes |
false |
Never prompt; fail instead. For automation - requires --project, and --billing-account if billing is not yet enabled. |
One region drives all three services. Localess uses gen-2 Cloud Functions triggers, which must be co-located with the Firestore database and Storage bucket they listen to, so splitting them apart produces functions that deploy but never fire.
--regionis permanent. Neither the Firestore database nor the default bucket can be moved after creation — changing your mind means a new project. The Functions region follows the database: sync and deploy always rewriteLOCALESS_REGIONin.env.<project-id>from the live Firestore location, so editing it by hand does not survive the next deploy.
Omit --region and setup asks for it — zone first (Europe, United States, Asia…), then the
exact region within that zone. Only the 40 regions where all three services exist are
offered; the list lives in scripts/localess/regions.mjs. The Firestore multi-regions (eur3,
nam5) are deliberately excluded — Cloud Functions has no multi-region equivalent to pair
them with.
Passing an unsupported --region is not a hard failure: setup says why and asks instead. Under
--yes it exits, since it cannot ask. Under --yes with no --region and no existing Firestore
database, setup uses the default, europe-west6.
The removed flags --location, --storage-location and --google-support-email are not
silently ignored: passing one prints a hint saying what replaced it (--region for the first
two; the Firebase console for Google sign-in).
Linking billing has cost implications, so an automated run (--yes) must name the account
explicitly with --billing-account rather than have one chosen for it.
npm run localess:setup -- --project <project-id>Every step is idempotent, so this doubles as a health check: it reports what already exists and provisions anything missing.
If all you want is to refresh the local files, use npm run localess:sync instead — it regenerates
them from the live project without re-checking any infrastructure. See
Keeping local files in sync.
Setup provisions infrastructure only — it never ships the application. It finishes by asking
whether to deploy, defaulting to no; --yes skips the question and does not deploy.
You do not need to pass --region again. When a Firestore database already exists its location
is read back and used for everything, because that location is immutable and therefore the
authoritative answer. Passing a --region that disagrees with it is an error rather than a
silent rewrite — otherwise re-running setup on a project provisioned outside Europe would
quietly point Functions away from its own data.
Setup does not apply the auth block from firebase.json — that happens on every deploy.
See Authentication.
In order:
1. Preflight firebase-tools present and >= 15.29.0, CLI authenticated
2. Project adopt --project, or pick from a list / create one
3. Billing link a billing account, then wait for it to become active
4. APIs check the 15 required APIs, enable whichever are off
5. Firestore adopt the existing location, or ask and create in --region
6. Storage create and link the default bucket in --region
7. Storage CORS allow GET and HEAD from any origin, when the bucket has no rules
8. Web app register a "Localess" web app
9. Hosting create the site (normally already present)
10. Mark project label it localess-managed with the version
11. Local config write .env.<project-id>, firebase.<project-id>.json, functions/.env.<project-id>,
firebase-config.<project-id>.json, firebase-config.build.json
12. Offer deploy ask whether to run `npm run localess:deploy` (defaults to no)
firebase deploy auto-enables most Functions-related APIs, but not all — most importantly
translate.googleapis.com, which is only used at runtime by
functions/src/services/translate.service.ts and so is never ensured by any deploy.
firebase firebasehosting firebaserules
firestore identitytoolkit firebasestorage
firebaseextensions cloudfunctions cloudbuild
artifactregistry run eventarc
pubsub storage translate
firebase.json carries an auth block, which the auth deploy target sends to Google's
provisioning API:
"auth": {
"providers": {
"emailPassword": true
}
}This initializes Identity Platform (required by the blocking functions in
functions/src/users.ts) and enables Email/Password.
Responsibility splits on a single line: setup enables services, deploy applies
configuration. Setup enables identitytoolkit.googleapis.com; the provider itself is
provisioned by auth, which is one of deploy's default targets. So Identity Platform is live
after your first npm run localess:deploy, not after setup.
Localess provisions email/password only. Google and Microsoft sign-in are configured in the Firebase console — Google needs an OAuth support email, Microsoft an Azure app registration.
Enabling a provider in the console is only half the job. The login UI reads
environment.auth.providers(src/app/auth/login/login.component.ts), which is baked in at build time fromLOCALESS_AUTH_PROVIDERSin.env.<project-id>. See First Deploy.
Any parameter you do not pass is asked for. Pass it and the matching prompt is skipped, so the same command works interactively and in a script.
Run without --project and setup lists every project your Firebase account can see, plus a
Create a new project... entry at the end of the list. Above twelve projects the list becomes
filterable — start typing to narrow it.
Projects Localess already knows about are sorted to the top and annotated with the version and region recorded on the live project:
❯ Localess Production (localess-prod) — local config · Localess 4.0.0 · europe-west6
Localess Staging (localess-staging) — Localess 4.0.0 · europe-west6
Stale (stale-config) — local config · no Localess marker
Unrelated (unrelated-app)
The same picker appears in npm run localess:sync, npm run localess:deploy and
npm run localess:check, minus the Create a new project... entry — none of them may create a
project.
Two independent signals combine:
| Annotation | Meaning |
|---|---|
local config |
A .env.<project-id> for it exists on this machine |
Localess <version> · <region> |
The live project carries the localess-version and localess-region labels |
Localess web app |
No label, but a web app named Localess exists (older projects) |
no Localess marker |
Configured here, but the live project shows no sign of Localess |
could not verify |
The remote lookup failed |
Local config is never trusted on its own — it can be stale if the project was deleted or rebuilt elsewhere — so it is always cross-checked against the live project. Equally, a project carrying the label is shown even when this machine has never configured it, which is what makes the list useful after a fresh clone.
The labels for every project arrive in a single Cloud Resource Manager call, so annotating the whole list costs one request rather than one per project.
Run without --region, on a project that has no Firestore database yet, and setup asks for the
zone and then the region.
Creating a project prompts for an id and a display name. The id is validated against Google's rules (6–30 characters, lowercase letters, digits and hyphens, starting with a letter) before anything is sent, so a typo is caught in the prompt rather than by a failed API call.
If billing is not yet enabled, setup lists your open billing accounts and asks which to link. It never picks one for you — linking billing has cost implications.
--yes disables every prompt for automation. It then requires --project and, if billing is
not already enabled, --billing-account.
If you pick a project with no sign of Localess, setup stops and asks before touching it, listing what cannot be undone — enabling billable APIs, and creating a Firestore database and Storage bucket whose locations are permanent. It defaults to no. A project already carrying the marker skips the question.
Ctrl-C at any prompt exits cleanly; every step is idempotent, so re-running resumes.
Setup labels the project it provisions, using the same mechanism Firebase uses for its own
firebase: enabled label:
| Label | Value |
|---|---|
localess-managed |
true |
localess-version |
the Localess version that is live, e.g. 4-0-0 |
localess-region |
the region Firestore, Storage and Functions share, e.g. europe-west6 |
Setup writes all three. Deploy refreshes localess-version after a successful push, so the
label answers "what is running", not "what provisioned this".
Labels are visible in the Google Cloud console and survive a fresh clone, which local
.env.<project-id> files do not. Older projects predate the label and are recognised instead by
a web app named Localess, which is weaker — setup only names an app when it creates one, so a
project that already had a web app never got it. Re-running setup adds the label.
Writing the labels is mandatory. npm run localess:deploy and npm run localess:sync refuse a project that
has no localess-managed label, so a silent failure here would leave a fully provisioned
project that could never be deployed to. If setup cannot write them it stops and says so; the
account needs resourcemanager.projects.update. Grant it and re-run — setup is idempotent.
The localess-region label is a display hint for the picker. The live Firestore location
always outranks it, because that location is immutable and the label is not; when they
disagree, sync and deploy correct the label.
All of these are gitignored. Setup and deploy never modify a tracked file, so git pull
from upstream never conflicts with your deployment.
| File | Contents |
|---|---|
.env.<project-id> |
Your deployment decisions. The only file you edit by hand. |
src/environments/firebase-config.<project-id>.json |
Web SDK config, fetched with apps:sdkconfig |
src/environments/firebase-config.build.json |
A copy of the above for the project being built |
functions/.env.<project-id> |
REGION=<region>, read by functions/src/index.ts |
firebase.<project-id>.json |
firebase.json with the /api/v1/** rewrite region applied |
All of them are written by the same code path, shared by setup, sync and deploy, so they
cannot drift apart depending on which command you ran.
src/environments/firebase-config.json — without a project id — is tracked, and holds a
demo-localess-dev placeholder. That is what npm start, npm run build:docker, npm test
and a bare npm run build:prod compile against, and a demo- prefixed project id is what makes
the Firebase emulators run fully offline. npm run build:deploy swaps in
firebase-config.build.json through the deploy configuration in angular.json, so a real
project's config is never written over a tracked file.
The tracked placeholder contains an
apiKey. That is not a leak — Firebase web API keys are public by design and ship in every browser bundle. It identifies a throwaway demo project.
The filename of .env.<project-id> is authoritative for the project id. Keep as many as
you have projects and select between them with --project.
A fresh clone builds with no generated files at all: the placeholder is tracked, and the
LOCALESS_* settings are compile-time constants defaulted in angular.json. There is no
postinstall step and no generated env.ts; see
Build-time configuration.
The Functions region goes in functions/.env.<project-id>, not functions/.env.
firebase-tools loads .env first and then .env.<project-id>, so the per-project file
wins — two configured projects cannot overwrite each other's region.
That leaves plain functions/.env free for values shared by every project, which is where
the DEEPL_API_KEY and UNSPLASH_API_KEY that functions/src reads belong.
Setup and deploy only ever write the
.env.<project-id>of the project they were run for.functions/.env, other projects' files and any environment file you maintain by hand are never read, rewritten or deleted. Everything they generate is gitignored, so no key can be committed.
The modules under scripts/:
| File | Responsibility |
|---|---|
scripts/localess.mjs |
Entry point — subcommand dispatch, usage, error and abort handling |
scripts/localess/commands/setup.mjs |
Provisioning: the infrastructure steps and the markers |
scripts/localess/commands/deploy.mjs |
Build and push, behind the marker gate |
scripts/localess/commands/sync.mjs |
The single writer of the local project files |
scripts/localess/commands/check.mjs |
Health check: fetches state, renders findings, applies --fix |
scripts/localess/checks.mjs |
The pure checks behind check — facts in, verdicts out |
scripts/localess/admin-user.mjs |
Creating the first admin user from the CLI (replaces the removed setup callable) |
scripts/localess/firestore-rest.mjs |
Encoding plain values for the Firestore REST API |
scripts/localess/projects.mjs |
Project identification, annotation and the deploy gate |
scripts/localess/firebase-cli.mjs |
Documented firebase <command> calls, spawned as child processes |
scripts/localess/firebase-gaps.mjs |
The steps that have no CLI command |
scripts/localess/firebase-tools.mjs |
Locates the global firebase-tools, enforces the minimum version |
scripts/localess/apis.mjs |
The required Google Cloud APIs, and the check both setup and deploy run |
scripts/localess/bucket-cors.mjs |
The default Storage CORS rules, applied when the bucket has none |
scripts/localess/config.mjs |
The .env.<project-id> format and project resolution |
scripts/localess/generate.mjs |
Turns a config into firebase.<id>.json and functions/.env.<id> |
scripts/localess/defines.mjs |
Turns a config into --define flags for the Angular build |
scripts/localess/deploy-plan.mjs |
Deploy targets and the firebase deploy argv |
scripts/localess/prompts.mjs |
Interactive pickers and input validation |
scripts/localess/regions.mjs |
Regions where Firestore, Storage and Functions all exist |
scripts/localess/markers.mjs |
How a project is recognised as Localess-managed |
scripts/localess/log.mjs |
The step logger shared by all four commands |
scripts/localess/usage.mjs |
UsageError, so a bad invocation prints usage rather than a failure banner |
scripts/generate-firebase-json.mjs |
Cloud Build entry point for writeFirebaseJson (used by cloudbuild.yaml) |
Several steps have no firebase command, verified against firebase-tools 15.29.0:
| Gap | Evidence |
|---|---|
| Enabling APIs | No firebase services:enable command exists |
| Linking billing | No firebase billing:* commands exist |
| Creating the default bucket | lib/gcp/storage.js only reads the bucket (getDefaultBucket) |
| Project labels | No firebase projects:* command reads or writes labels |
| Bucket CORS | No command reads or sets a bucket's CORS rules |
| Cloud Run invoker IAM | No command reads or grants run.invoker on a function's service |
| Identity Platform config and users | No command reads the config or queries users |
| First admin account | No command creates an account or sets custom claims |
| Firestore writes | No command commits documents (used when bootstrapping the first admin) |
They are reached through firebase-tools' internals — apiv2.Client (an authenticated HTTP client
that carries the cloud-platform scope, see lib/scopes.js) and ensureApiEnabled. Those are not
public API, so they are confined to this one file: an upstream breaking change is a single-file fix,
and firebase-tools.mjs fails loudly if the installed version is older than the one this was
verified against.
Provisioning authentication is not in this file. Since 15.29.0 the auth block plus
firebase deploy --only auth covers Identity Platform and the providers, so it goes through the
normal CLI path. Only the reads and the admin-account writes that check needs live here.
apiv2 keeps its refresh token in a module-level variable that only the CLI's own command wrapper
populates. Importing the internals standalone therefore fails with "not yet authenticated" even
when the user is logged in. firebase-gaps.mjs calls requireAuth() first, which also requests
the cloud-platform scope and falls back to application default credentials.
Your billing account has hit its limit on linked projects. Either unlink a project you no longer need, or request an increase at https://support.google.com/code/contact/billing_quota_increase.
A fresh billing link takes up to a minute to reach Service Usage. The script waits for billing to report active and retries the Blaze-gated APIs, so this resolves itself. If it persists, verify the link in the console.
A just-enabled API has not propagated. Retried automatically. The retry deliberately does not
apply when the response carries a concrete violation (QuotaFailure, PreconditionFailure,
ErrorInfo) — those are real failures and fail fast.
Almost always the Spark plan: new projects require Blaze for a default Storage bucket. Confirm billing is active first.
The Firebase CLI intermittently aborts while tearing down its event loop, after the command has
already completed its work. Exit code 3221226505 (0xC0000409). Because every command used here
is idempotent, firebase-cli.mjs retries this specific code up to three times.
Handled: apps:sdkconfig --out refuses to overwrite, so the script does not use it. It fetches
the config as JSON and writes the file itself, which keeps the command idempotent and leaves no
temp file behind when a run fails partway.
firebase projects:list can lag minutes behind project creation. The script validates with
firebase use instead, which is authoritative.