Most GeoLens installs are prebuilt-image installs, the ones created with
curl -fsSL https://getgeolens.com/install.sh | sh. Their .env records
COMPOSE_FILE=docker-compose.prod.yml and a pinned GEOLENS_VERSION. For these,
the recommended upgrade is the one-command, backup-first flow:
Terminal window
# From your install directory
./scripts/upgrade.sh# upgrade to the newest published release
./scripts/upgrade.shX.Y.Z# or pin an explicit target version
scripts/upgrade.sh (equivalently, bash scripts/install.sh --upgrade) performs, in order:
Syncs release files, then pulls images. Where the install is a git checkout it checks the target tag’s compose files and mounted helper scripts out over the install (a non-git install warns and continues pull-only); then docker compose pull --ignore-buildable. The app keeps serving throughout, so the download costs no downtime.
Stops api + worker, then takes the pre-upgrade backup — pg_dump -Fc to a timestamped file under backups/pre-upgrade/. The outage starts here. The upgrade aborts if the dump fails, is empty, or does not read back cleanly end to end (nothing else is touched).
Runs migrations via the fail-closed one-shot migrate service. A failed migration aborts before the app is started.
Pins the new GEOLENS_VERSION in .env, once the migrations have committed.
Starts the stack and waits for every service to report healthy.
On success it keeps the pre-upgrade dump and prints the rollback
recipe for reference; on any failure it stops, leaves your data in the dump,
and prints the same recipe. If it fails while the app is stopped for the
migration, it brings the previous version back up first and checks that it
stayed up, so a failed upgrade does not leave the instance down. The exception
is a migration that already committed a revision the old release has never heard
of — that release cannot boot against the new schema, and restoring the
pre-upgrade dump is then the only way back.
If you instead build from source (git clone install, COMPOSE_FILE=docker-compose.yml),
upgrade by updating the checkout and rebuilding. See
Source-build upgrade below. scripts/upgrade.sh
detects a source install and prints those instructions instead of running.
Back up your database before any major upgrade. Use a custom-format (-Fc) dump, the format scripts/restore.sh expects for rollback. Skip this step entirely if you are running scripts/upgrade.sh in step 4 — it takes the same backup for you, and it pulls the new images before it stops anything, so the download costs no downtime. Do it by hand only when you are driving the upgrade manually, and pull the images first if so.
Terminal window
# Stop the writers first. pg_dump does not block writers, so a write
# acknowledged during the dump would be missing from the backup.
Use /api/health (proxied to the API), not a bare /health — through
the bundled Nginx a bare /health is answered by the frontend SPA with
HTML and status 200, so it can never fail even when the API is down.
Rollback is re-pin the previous version + restore the pre-upgrade -Fc dump.
Schema migrations move forward only; alembic downgrade is not a supported
rollback, and you must neverpsql < dump a custom-format (-Fc) file (it is
not plain SQL). Restore with scripts/restore.sh, which validates the dump and
restores it via pg_restore.
Terminal window
# 1. Re-pin the previous version in .env (edit the GEOLENS_VERSION= line).
# Image tags are bare semver, e.g. the version you upgraded FROM:
# GEOLENS_VERSION=X.Y.Z
# 2. Restore the pre-upgrade dump the upgrade created. restore.sh stops
# api/worker, runs pg_restore, and restarts them afterward.
A failed scripts/upgrade.sh run prints this exact recipe (with the real dump
path filled in) before it exits, so you can copy it from the upgrade output.
For installs that build from source (COMPOSE_FILE=docker-compose.yml),
upgrade by updating the checkout and rebuilding instead of pulling images. Take
the -Fc backup first (step 1 above, substituting -f docker-compose.yml),
which stops the writers, then:
Terminal window
# 1. Stop the writers FIRST if they are still running. The outage starts here.
# Unlike the prebuilt flow, the build cannot happen while the app serves:
# this compose file bind-mounts ./backend/app into the api container, so
# checking out the new tag in step 2 swaps the running app's code the
# moment it lands.
dockercompose-fdocker-compose.ymlstopapiworker
# 2. Update the checkout to the new release tag and rebuild.
gitfetch--tagsorigin
gitcheckoutvX.Y.Z# replace with your target tag
dockercompose-fdocker-compose.ymlbuild
# 3. Run migrations (fail-closed) BEFORE starting the app.
Each release’s authoritative record of what changed is its
CHANGELOG
entry. Read the target version’s section — Added, Changed, Fixed, Removed,
Security — before upgrading; operator actions are called out there. A few
older releases also carry an explicit Upgrade notes heading. The notes
below cover the releases with material operator action.
1.26.0: dataset retention columns, bytea rows and raster tile 404s
Migrations 0084, 0085 and 0086 run on upgrade. 0084 adds a nullable
api_keys.dataset_id, 0085 adds three dataset retention columns to
catalog.datasets, and 0086 widens the API key scope checks. All three are
additive and need no backfill. Nothing is deleted until a retention setting
is saved. 0085 gives up after 5 seconds waiting for its table lock instead of
queueing behind other writes, so re-run the upgrade if it reports a lock
timeout.
The downgrades of 0084 and 0086 refuse while an active dataset write or
append-only key exists. Restore the pre-upgrade dump instead, as in
Rollback.
GET /datasets/{dataset_id}/rows/ returns bytea values as \x hex
strings. A client that read a UTF-8 bytea value as text must decode the
hex.
Anonymous requests for a non-public raster tile or tile token answer 404
instead of 401. A client that treated that 401 as a cue to sign in needs
another signal. Requests with an expired or invalid credential still answer
401.
Extension authors:EXTENSION_API_VERSION is now 18, and
ProcessingPort has a new required vrts_beyond_member_audience.
Upgrade to 1.25.1, not 1.25.0. The 1.25.0 release published its Python
and TypeScript packages and its frontend and backup images, but no API or
worker image, so ./scripts/upgrade.sh 1.25.0 fails at the image pull.
1.25.1 is the first release with those images and carries everything listed
here.
Migrations 0082 and 0083 run on upgrade. 0082 adds two small login
throttle tables. 0083 builds one index and widens another on ingest_jobs
and records, and writes to those two tables wait while it runs.
BACKUP_RETENTION_DAILY and BACKUP_RETENTION_WEEKLY now count days and
ISO weeks, not dumps. Check both values. With one backup a day the result
is the same. See Backups & Restore.
The default Content-Security-Policy no longer allows
https://static.cloudflareinsights.com. Cloudflare Web Analytics users set
CSP_SCRIPT_SRC_EXTRA=https://static.cloudflareinsights.com. Use the host,
not the beacon.min.js file: Cloudflare loads the beacon from a versioned
path under it, which a source naming the file doesn’t match.
Outbound TLS from the AI providers and the OAuth client checks the operating
system trust store instead of certifi. Add a private CA to the system store
or point SSL_CERT_FILE at it.
Only administrators can discover and register database tables. Accounts
with only upload now get 403 from the register endpoints.
Accounts throttle sign-in to unknown browsers after 10 failures in an
hour. Shared service accounts should use API keys. To let one account sign
in again from a new browser, run
docker compose exec api /app/.venv/bin/python -m scripts.trust_login_device <username>.
It prints a single-use link, valid for 10 minutes, to open in that browser;
the person still has to enter the password. See
Sign-in throttling.
On Anthropic’s own API, installs that do not set LLM_MODEL move to
claude-sonnet-5-5 and claude-haiku-5-5. Bedrock and Vertex have no
default model, so set LLM_MODEL to the platform’s model ID; see
Claude on a cloud platform.
If you set VALKEY_PASSWORD, put the same password in REDIS_URL and
recreate valkey, api and worker together.
Set PUBLIC_APP_URL for per-map link previews.
Unsigned anonymous requests for a non-public vector tile answer 404
instead of 403. See Tile endpoints.
1.23.0: shared credential store, Titiler relay and migrations
Migrations 0078, 0079 and 0080 run on upgrade. 0078 adds nullable
original_srid, is_3d and n_dims columns to dataset versions. 0079 adds
datasets.data_revision and the refresh run’s data_revision_baseline.
0080 adds 1 to the tile version of every dataset with a
timestamp with time zone column, so tile caches stop serving the old text.
None needs a backfill. Runs admitted before 0079 are not compared for
live-data changes.
Set REDIS_URL to a shared Valkey or Redis instance before importing,
re-uploading or refreshing a token-protected service. Without it the API
answers 503 credential_store_unavailable. See
Cache.
Set REMOTE_RASTER_RELAY_BASE_URL if Titiler reaches the API at an
address other than http://api:8000, and make sure Titiler can connect to
it. The default needs no change under either Compose file. If Titiler’s
traffic goes through a forward proxy, add the API host to NO_PROXY.
Browser sessions on a deployment whose API is on another origin or path
end when the page reloads. Same-origin deployments recover from the refresh
cookie.
A CDN rule that overrides origin cache headers is not bounded by
s-maxage. Public tiles stay cached for that rule’s TTL.
Clients that read a refused workflow transition’s 422 detail as a string
must read detail.message.
Source-build installs resync the dev frontend volume with the lockfile on
start, so the first start after an upgrade can take a minute longer.
Migration 0077 runs on upgrade. It adds nullable columns for the previous
version and the scheduled refresh hold, adds restored_from_version to
dataset versions, and widens the refresh run origin check to allow
restore. It needs no backfill and can be rerun after an interruption.
Rolling back past 1.22 can leave storage objects behind. The previous
release’s retention purge does not recognize the new follow-up record, so it
can delete a job past retention (30 days by default) that still owes cleanup,
and a superseded storage object such as a replaced COG or archive is then
never deleted. No live dataset data is affected. GeoLens ships no sweep for
this, so use your storage provider’s tooling or an inventory diff against the
catalog.
Rolling back across migration 0077 drops the kept previous-version
tables, and the downgrade fails while a restore run exists.
Custom clients that commit a file replacement without a
review_fingerprint now end with a blocked run whenever the replacement
needs review. Send the fingerprint from the preview response after a person
has seen it.
Record extents for 3D Tiles datasets published before GeoLens read box
and sphere bounding volumes. Those datasets have no extent, so search
shows “No Extent” and they have no footprint. Run the backfill once after
upgrading; it reads each stored tileset.json and only touches 3D Tiles
datasets that have no extent, so a rerun is safe:
The API container’s root filesystem is read-only, so uv run cannot create
its cache there; call the baked virtualenv’s Python directly, as above. Add
-f docker-compose.prod.yml when that is the file your install runs. With
GEOLENS_TENANCY_MODE=multi_tenant, run both commands once per tenant with
--tenant <tenant id>; without it the script exits 2 before reading anything.
SELECT id, slug FROM catalog.tenants; lists the ids.
The script lists each dataset it updates, skips or fails, with the reason.
Backfilled datasets show a new updated time. A tileset in a local frame is
skipped, since it has no extent to record. It exits 1 when a dataset’s
tileset pointer or stored tileset.json is missing or unreadable, and 2 when
it cannot start the way the worker does, for example when an overlay the
install needs is not loaded.
1.20.0: verified refresh; extension API version 10
Overlay packages must be rebuilt.CatalogPort now requires
verified_refresh_service_task(), and the extension API version is 10. This
gives verified refreshes a task name that older workers cannot process as an
ordinary re-upload during a rolling deployment. An overlay that declares a
different version is refused at load. Installs without overlay packages are
unaffected.
Migration 0063 adds durable refresh review state. Run migrations before
starting the API and workers, as in the standard upgrade flow.
Migration 0061 adds refresh-token families. Apply it with all API
processes stopped, then start all of them on the new version; mixed versions
cannot keep a family intact through rotations an older process handles. The
standard upgrade flow does this. Existing refresh rows get independent family
IDs, because the old parent and child links were never stored, and keep their
expiries.
Reusing a rotated refresh token revokes its family. A rotated token still
works during the concurrency grace window (30 seconds by default). Reuse after
that revokes the family’s refresh tokens while the presented token is within
its original lifetime (seven days by default); a naturally expired token just
returns 401. POST /api/auth/logout/session/ revokes one family and leaves
other devices and API keys alone. POST /api/auth/logout/ still revokes
everything for the account. Downgrading 0061 revokes spent refresh rows first,
so an older server cannot accept them again.
A replacement with no geometry is refused over a dataset that stores
geometry. The re-upload preview stops with a message saying the dataset
would become a plain table, so nothing was changed, and that the file belongs
in a new dataset. A commit that skipped the preview is accepted, and its job
then fails with the same message, leaving the dataset as it was. A service
refresh whose layer arrives without geometry fails the same way, before the
swap, and the existing copy keeps serving. A Parquet replacement over the
ingest row or cell ceiling now gets the ceiling message in the re-upload
preview instead of being reported as malformed. No operator action.
srid_override must be an EPSG code the instance knows. An import
commit or a re-upload commit (geolens replace --srid) naming a code that
neither PROJ nor PostGIS has is refused with a 422 naming the code, and a
manifest metadata.crs with such a code is reported as an error for that
entry, in --dry-run too. Such a code used to be accepted and then ignored,
so the data was read under the detected CRS. Check any script that passes
one.
Manifest metadata.license, metadata.organization and metadata.tags
now reach the dataset, when the manifest creates it and on a later apply
that updates the entry. License and organization overwrite the stored value
whenever the entry carries one; tags are added to the dataset’s keywords and
are never removed by a later apply. An unchanged entry still skips, so a
dataset created by an earlier release keeps its current values until its
entry changes. See
Manifests.
SANDBOX_QUERY_SLOTS is new and optional. Under
DB_USE_EXTERNAL_POOLER the SQL sandbox slots each API worker admits are now
4 divided by UVICORN_WORKERS instead of 4 per worker, since every
worker draws on the one pooler. Set SANDBOX_QUERY_SLOTS to state the
per-worker share yourself when several API replicas share the pooler.
Installs without an external pooler are unaffected. See
Configuration.
No migration ships in 1.19.1.
1.19.0: shared rate-limit store; tile links retire on unpublish
Set REDIS_URL if you run more than one API worker. Rate limits are now
counted in that shared store when it is set, and once per API worker when it
is not. The bundled production compose file runs UVICORN_WORKERS=2 by
default, so on an install without the store a configured 60 requests a second
is enforced as 60 per worker. Limits are never switched off by the absence of
a store, only counted separately, and a configured store that goes unreachable
falls back the same way: it logs rate_limit_storage_unreachable, then
rate_limit_storage_recovered when it returns. Each API worker logs
rate_limit_storage_not_configured at startup while the variable is unset,
and a URL whose scheme is neither redis:// nor rediss:// logs
rate_limit_storage_scheme_unsupported and counts per worker as well.
Tile links already handed out stop working when a dataset is unpublished or
made private. A signed tile template used to keep serving for the rest of
its window. The raster, vector and cluster routes now all retire outstanding
templates on that transition. The counter behind this rolls on any
publication-status or visibility change, in either direction, so publishing
a dataset, or making it public again, retires its outstanding templates too. The
application caches a dataset’s metadata for a minute, so the change takes
hold within that. Ordinary edits, reuploads and replaces leave live templates
alone. Re-mint any template pinned in a script or a saved GIS project after
changing either setting. See
Tile endpoints.
Migration 0060 adds datasets.publication_version. It is a plain column
add with a server default of 0, and it is what a signed tile link now
binds to. No operator action beyond running the migration.
Importing from a file URL now downloads in the background. The endpoint
answers once it has validated the request instead of holding the connection
open, so a large file or a slow origin is no longer bounded by your proxy’s
read timeout. URL_IMPORT_FETCH_MAX_SECONDS caps a single download and
defaults to 1800 (30 minutes). Two things change for an API caller: the
import response’s status is no longer always pending, and a job’s step can
read downloading. The fetch runs on the download queue, which the default
worker already subscribes to, so at the stock WORKER_CONCURRENCY=1 a slow
origin holds that worker’s only slot for the whole transfer. To keep long
downloads off ordinary imports, run a second worker with
WORKER_QUEUES=download and drop download from the main worker’s list.
strict_cog on a raster import is honoured. The commit body omitted the
field, so it was dropped before the raster path read it and every import
converted a non-COG GeoTIFF whatever the caller asked for. Sending true now
fails the job instead of rewriting the file. Combining it with resampling,
nodata_override, srid_override, or a compression other than the default
DEFLATE is refused with a 422 naming the fields that clash. No caller could
set the field before this release, so nothing that used to work behaves
differently.
1.18.0 and 1.18.1 sent a WFS or OGC API Features import that carried a Basic or
named-header credential to a queue named ingest-auth-v2, so a worker from an
older release never picked up a job it could not read. 1.18.2 sends those
imports to the default ingest queue again. The default WORKER_QUEUES still
lists ingest-auth-v2, so a worker drains anything a 1.18.0 or 1.18.1 API left
there, and logs a warning at startup when an override of WORKER_QUEUES omits it.
From 1.18.0 or 1.18.1: upgrade the API and worker together, or the API
first. Nothing is stranded either way.
From a release before 1.18.0: upgrade the worker before or with the API.
A worker that old dequeues a header-auth job from ingest, cannot parse it,
and fails the import after using up its single-use credential.
Stranded jobs: a rollback to a release before 1.18.0, or a
WORKER_QUEUES override without the name, leaves jobs todo on
ingest-auth-v2 indefinitely. The runbook has the
query that counts them and the steps to clear them.
Overlay packages must be rebuilt. The extension API version is now 9,
bumped twice in this release. CatalogPort gained a required
resolve_embedding_config, generate_embedding takes the resolved
embedding configuration as a pin, get_record_embedding returns the row’s
model and fingerprint alongside the vector, get_embedding_distances takes
that pair as required keywords, and get_nearest_record_ids takes the
caller’s already-read anchor — the vector together with that pair. An
overlay that declares a different version is refused at load. Installs
without overlay packages are unaffected.
No re-embed is required. Rows written before this release keep matching
on model name alone until they are regenerated; upgrading changes nothing an
operator sees and triggers no re-embed.
1.13.0: extension API version 7; dataset delete detaches registered tables
Overlay packages must be rebuilt. The extension API version is now 7.
An overlay built against an earlier version is refused at load, so rebuild
every overlay package before deploying this release. Installs without
overlay packages are unaffected.
Deleting a registered dataset no longer drops its table. The catalog
row, grants, tiles, and caches are removed while the operator’s physical
table survives. Datasets GeoLens ingested itself are still dropped in full.
The delete dialog states which of the two will happen. If you had scripted
around the old drop-everything behavior, re-check it.
1.12.0: raster CRS override assigns instead of reprojecting
A raster CRS override now assigns the CRS rather than reprojecting to
it. Supplying an EPSG code at import or replace time relabels the raster
in place: pixel values, the pixel grid, and the corner coordinates all pass
through untouched, and only the CRS they are read under changes. A dataset
ingested with an override therefore lands wherever those coordinates put it
in the CRS you named, which may not be where the same override put it
before. Rasters are still reprojected at serve time and at export time;
reproject-at-ingest is not offered. Re-check any ingest automation that
passed an override expecting a reprojection.
Because the conversion no longer resamples, the uploaded file is deleted
after a successful lossless ingest instead of being kept as a second
permanent copy.
Migration 0041 is a data migration. It rewrites stored WKT1 CRS
definitions to WKT2:2019 in batches; deploy logs report
converted to WKT2:2019: N row(s). The downgrade is a no-op.
1.11.0: shared credential store for protected service refreshes
Passing a service token to a refresh now requires REDIS_URL
(Valkey/Redis), so the credential can reach the worker without touching
disk. Deployments that only refresh public sources need no change.
1.10.0: PROMETHEUS_MULTIPROC_DIR for multi-worker metrics
New env var, required for multiprocess metrics: PROMETHEUS_MULTIPROC_DIR.
Both the dev and prod Compose files set it to a tmpfs-backed path. On a
custom deployment running UVICORN_WORKERS > 1, point it at a writable
directory that is empty on boot, or /metrics serves single-process
(non-aggregated) counters.
Pull or rebuild before recreating the api container. The entrypoint
script that prepares the directory ships in the image, not in the Compose
file, so recreating api to pick up the new variable without a new image
fails to boot.
1.7.0: restart the backup container; two endpoint shapes changed
Restart the backup container once after upgrading (any full stack
restart covers it). The backup daemon reads its entrypoint at container
start, so an already-running daemon keeps the old cycle and will not
produce the new globals-*.sql role dump until it restarts. That artifact
is what makes a fresh-cluster restore work — see
Backups & restore.
Antimeridian-crossing extents are now served in RFC 7946 spec form, a
bbox with west > east (e.g. [178, -19, -178, -17] for Fiji) at the
dataset and collection endpoints, instead of a flattened [-180, …, 180]
span. Clients that assume west <= east need to handle the wrap.
GET /tiles/raster-auth-check/ is gone from the API contract and both
SDKs. Anyone calling it directly should use the raster tile URL from
GET /tiles/token/{dataset_id}/ instead.
DEMO_MODE is ignored. The Demo Mode banner was replaced by the
admin-configurable Site Banner (Settings -> General), which carries custom
text, a color choice, and per-session dismissal. Env-only deployments
configure it with BANNER_ENABLED, BANNER_TEXT, and BANNER_COLOR. The
/api/auth/config response no longer includes the demo_mode field.
Set BACKUP_MAX_AGE_MINUTES if you run a non-daily BACKUP_SCHEDULE.
The backup container now reports unhealthy when backups stop succeeding,
judged against this setting (default 1560 minutes = 26 hours, sized for the
default daily schedule). Roughly 1.5x your backup interval is the right
value.
1.5.0: PostgreSQL 18 + PostGIS 3.6 (breaking for self-hosted)
scripts/upgrade.sh does not apply to this release. The bundled database
moved from PostgreSQL 17 + PostGIS 3.5 to PostgreSQL 18 + PostGIS 3.6. An
existing PG 17 pgdata volume cannot be opened by PG 18, so the upgrade is a
dump -> fresh volume -> restore, not an image pull.
Bundled-database installs: follow the procedure in
RUNBOOK.md section 6.
scripts/upgrade.sh compares the running server’s major version against the
one the target release bundles and stops before anything changes — no
image pull, no version pin, no database write — printing that procedure
instead.
Managed or external database installs (DATABASE_URL_OVERRIDE): the
check is skipped, since the bundled image’s version says nothing about the
database you actually use. Run your provider’s PostgreSQL 17 -> 18 upgrade,
then deploy as usual. The minimum supported external version is
PostgreSQL 15; the migrations refuse an older server.
The backup image’s pg_dump moves to 18 in lockstep, because a v17
pg_dump cannot dump a PG 18 server.
max_connections is raised to 80 in the bundled db/postgresql.conf
to cover the API-side job-queue connector. Recreate or restart the db
container after upgrading so the new value takes effect. A running
container keeps its old limit until it restarts.
DATABASE_SSL_MODE now fails closed. The undocumented allow and
verify-ca values no longer boot; switch to prefer or verify-fullbefore upgrading.
Back up before applying migrations 0018 through 0024. The standard
scripts/upgrade.sh flow takes this backup automatically. These migrations
add tenant identifiers, database roles, row-security policies, and tenant
data-schema support; default single-tenant deployments keep their current
behavior.
Run these migrations with a database role that can create roles.
Managed-PostgreSQL users may need a separate migration credential with
CREATEROLE; keep the API and worker on their existing least-privilege
runtime credential. On a busy database, lock-sensitive steps time out after
five seconds instead of queueing behind application traffic — retry during
a quieter window if that happens.
The backend now enforces a 32-character minimum on JWT_SECRET_KEY at startup (HS256 requires >= 256 bits of entropy). A deployment with a shorter secret will fail fast on the next restart with:
FATAL: JWT_SECRET_KEY must be at least 32 characters. Generate one with: openssl rand -hex 32
Before upgrading, verify your secret length:
Terminal window
echo-n"$JWT_SECRET_KEY"|wc-c
If it reports fewer than 32, generate a replacement and update your .env:
Terminal window
JWT_SECRET_KEY=$(opensslrand-hex32)
Rotating the key invalidates all issued JWT tokens. All users will be logged out and need to sign in again. Plan the rotation during a low-traffic window, or coordinate with your user base.
.env.example ships JWT_SECRET_KEY= empty, and the validator also rejects well-known public placeholders such as dev-only-change-me-in-production. scripts/install.sh generates a real openssl rand -hex 32 secret on first run, so installer-driven deployments are unaffected; hand-edited .env files must set a real openssl rand -hex 32 value.
Other env hardening in this release:
Secret fields (POSTGRES_PASSWORD, JWT_SECRET_KEY, GEOLENS_ADMIN_PASSWORD, ANTHROPIC_API_KEY, OPENAI_API_KEY, S3_SECRET_ACCESS_KEY, TILE_SIGNING_SECRET) are now stored as Pydantic SecretStr internally. Values are masked in logs, repr(), and validation-error output. Application behavior is unchanged; this is a defense-in-depth improvement.
LOG_LEVEL values are now validated against the stdlib logging set (DEBUG, INFO, WARNING, ERROR, CRITICAL). A typo like LOG_LEVEL=verbose now fails at startup instead of crashing later.
ENV_ONLY_CONFIG is now documented in .env.example. It is optional.
The backend/.env symlink has been removed. Host-side workflows (cd backend && uv run pytest) now resolve ./.env at the project root via the Settings env_file path. No action required unless you had local scripts depending on backend/.env as a literal path.
VITE_API_PROXY_TARGET (docker-compose.yml frontend service) was renamed to API_PROXY_TARGET. The old name still works for one release via a fallback in vite.config.ts; update your local compose overrides when convenient.
The landing page has been removed. The root route (/) now serves the Search page directly. The SHOW_LANDING_PAGE environment variable has been removed from backend config and the branding API.
What this means:
Existing bookmarks to / will show the Search page instead of the landing page.
The /search route redirects to /; existing /search bookmarks continue to work.
Remove SHOW_LANDING_PAGE from your .env if present (it is ignored but produces no error).
GeoLens 1.0.0 is the first public release. Prior to 1.0.0, the project was internally versioned as 2.0 -> 13.0 during pre-public development. Those legacy versions never shipped to anyone outside the project.
If you somehow have a checkout from a pre-1.0.0 internal build:
No data migration is required. The 1.0.0 schema is compatible with the most recent pre-public versions; Alembic migrations apply normally on the first 1.0.0 startup.
The version number resets, but the codebase moves forward. 1.0.0 is the cumulative state of all prior internal work, not a downgrade.
No git checkout v13.x rollback path exists from 1.0.0. If you need to roll back, restore from the database backup you took before upgrading (see Pre-upgrade checklist).
No environment variables changed at the 1.0.0 boundary. Your existing .env from any pre-public build continues to work without modification.
For all subsequent upgrades, follow the standard procedure above.