MethodDevelopment
verified checked against the code · what the statuses mean
Environment variables
In simple terms
All of the app’s data lives under one data root — by default
backend/data/ inside the repository. Every path the backend uses (the
database, stored results, the Parquet cache, the default assumption books, the
confidential folder) is derived from that root by one module,
backend/core/paths.py, and each can be moved on its own.
Nothing moves unless you set a variable. With no .env and no export, every
location is exactly what it was before this scheme existed.
Precedence
- a shell export (
export PTX_DATA_ROOT=…, or a variable set by the process that launches the server); - the repo-root
.envfile, read once at start bypython-dotenvwithoverride=False— it fills only what the shell left unset; - the default, listed below.
A relative value resolves against the repository root, never the working
directory: PTX_DB_PATH=backend/data/ptx_projects.db means the same file whichever
folder you start uvicorn from. ~ is expanded.
What the code reads (verified against paths.VARIABLES by a test)
| Variable | Default | What it moves |
|---|---|---|
PTX_DATA_ROOT |
backend/data |
The one data root; every path below defaults under it. |
PTX_DB_PATH |
<root>/ptx_projects.db |
The SQLite database (WAL side files sit beside it). |
PTX_AUDIT_DB_PATH |
audit.db beside the database (<root>/audit.db) |
The action log — who did what, when — in a SQLite file of its own (its own schema and version table; the main database’s migrations never touch it). Created by the first recorded event. Gitignored; back it up with the database. |
PTX_DATA_RESULTS |
<root>/results |
Stored feasibility solves, saved runs, finder studies, the every-combination log — one folder per project, plus the machine-level _grid_machine.json at the root. The test fixtures set it to a temp dir. |
PTX_DATA_PROCESSED |
<root>/processed |
The Parquet cache of open site series (site_datasets/<id>.parquet). |
PTX_DATA_RAW |
<root>/raw |
Raw timeseries workbooks. (Stored dataset paths are repo-relative and resolve against the series root — PTX_SERIES_ROOT, by default the repo root; this is the folder new ingests write to.) |
PTX_DAB_DIR |
<root>/dab |
The default assumption books shipped with the app and the committed ACTIVE marker (read-only to every workspace). A registry row’s relative file_path names a file in this folder by base name. |
PTX_WORKSPACE_DAB_DIR |
<root>/dab_workspaces |
Each workspace’s own default books, one folder per workspace (<dir>/<workspace_id>/dab_<version>.xlsx, owner-only files). The app writes a default book only here: a version that still points at a shipped file is copied in the first time it is changed. Stored in the registry as dab_workspaces/<workspace_id>/<file>, relative to this folder. Gitignored; back it up with the database. |
PTX_PRIVATE_DIR |
<root>/private |
Everything confidential that is not in the database: the encrypted source map, raw third-party inputs, and the Parquet cache of datasets flagged confidential (processed/<id>.parquet). Gitignored, refused by the pre-commit hook, never copied anywhere. |
PTX_EXPORTS_TMP |
the system temp dir | Where export temp folders (ptx-export-*) are created; they are deleted after the response is sent. |
PTX_BACKUP_DIR |
~/Desktop/PtX_backups |
Where the app writes the database backups it takes itself — today, before a workspace is deleted (ptx_projects-<stamp>-before-delete-workspace-<id>.db, made with the SQLite backup API, checked, mode 0600). Outside the data root, beside the hand-made backups. It must be a folder on this Mac — never a synced or shared one: the backup holds every project and every account’s password hash. The test suite and copy servers point it at a temp folder. |
PTX_SERIES_ROOT |
the repository root | What a relative dataset series_path resolves against (every stored series path is repo-relative). Set it to another checkout to read that checkout’s series in place — how a worktree runs the identical-solve gate on the measured series without copying or linking them (verify_db_copy.py --series-root). Read only by the series resolution (paths.resolve_data_file). |
PTX_AUDIT_KEEP_MONTHS |
12 |
How many months of the action log are kept row by row. At start-up and then once a day, older rows are folded into monthly counts per workspace and kind, and deleted. A value that is not a whole number of months (1 or more) is ignored with a warning. |
PTX_SOLVE_WORKERS |
2 |
How many engine jobs run at once on this server (core/solve_queue.py): a feasibility solve, a sweep, a Design lab study or search, “Try every combination”, the cost landscape, the check of which assumptions move the cost — and a request waiting on the LP (Least cost; Scale the plant and Grid on against off on the solver). The others wait their turn — first come, first served, except that a workspace with a job already running does not get a second worker while another workspace’s job waits — and the app shows them as Queued · N ahead (Feasibility suite, “When the server is busy”). A whole number, 1 or more; any other value is ignored with a warning in the server’s log and 2 holds. Read when the server starts. Set it to the cores you can spare for solving: an LP year holds one core and 1–2 GB for a minute or two. |
PTX_GRID_WORKERS |
min(13, cores − 2) |
Worker processes for “Try every combination” and the book-impact pool (core/grid_runner.py) — inside ONE job of the solve queue. |
PTX_DEV_PRINCIPAL |
unset | Development only. A user id: every request that carries no session cookie acts as that user, in their oldest workspace with their role there (api/auth.py). The test suite sets it to local so TestClient requests need no sign-in; a developer’s server may too. Unset, every request without a session is refused (401). An unknown or disabled user id is ignored (still 401), never replaced by a default. The server logs a warning at start when it is set — never set it on a server other people can reach. Scripts that drive the API directly (the identical-solve gate, the Excel engine) do not read it: they act as an explicit principal. |
PTX_DEV_ORIGINS |
http://localhost:5173,http://127.0.0.1:5173 |
The pages (origins) the API on 127.0.0.1 accepts requests from — the development frontend. Comma-separated; exact matches only. Only http://localhost:<port>, http://127.0.0.1:<port> or http://[::1]:<port> is accepted; any other entry is ignored with a warning in the server’s log (an entry list with nothing valid accepts no page at all). Read on every request (api/auth.py), and used by both the cross-site check and CORS. Before 2026-11-08 any localhost port was accepted. A copy frontend on another port (verification) names its own origin here, e.g. PTX_DEV_ORIGINS=http://localhost:5200. The shared HTTPS socket never uses it: there only this Mac’s sharing addresses count. |
PTX_PUBLIC_HOST |
unset (the container image: localhost) |
The container only (backend/scripts/serve.py --behind-proxy, what the image runs). The ONE host name people open: the app answers to that name only (with localhost: to this machine’s own names), and a change must come from that page. A host name or address alone — no scheme, port or path, never * or 0.0.0.0. Without a proxy (below) only localhost is accepted: plain HTTP to any other name would carry passwords in clear. Unset, --behind-proxy refuses to start; without that flag it is not read. See Run with Docker. |
PTX_TRUSTED_PROXIES |
unset | The container only. The proxy in front of the app (Caddy on a server): its addresses or networks, comma-separated (172.29.66.2, 172.29.66.0/24). The server believes X-Forwarded-Proto and X-Forwarded-For from those addresses only — the scheme decides the cookie, the client’s address is what the sign-in throttle counts — and refuses every request that did not come through it over HTTPS, the health route excepted. *, an address range covering everything, or a malformed entry is refused at start. Unset: no proxy, nobody’s forwarded headers are believed (the image on your own computer). |
VITE_API_BASE (frontend) |
http://localhost:8000 under the dev server; /api when the backend served the page |
The backend base URL, read by frontend/src/data/api.ts at build/dev time. Unset: the dev page (Vite) calls port 8000 on its own host; the built app served by the backend — backend/scripts/serve.py --share, or the container (--behind-proxy) — carries a <meta name="ptx-api-base" content="/api"> marker and calls /api on its own address (one origin, one cookie). |
Sharing on the office Wi-Fi (since 2026-11-04) adds no variable: it is switched on by how the
server is started (backend/scripts/serve.py --share, --share-port, --port), and its
certificate authority lives in PTX_PRIVATE_DIR (<private>/tls/, keys mode 0600).
In the container image every path variable above is set to an absolute folder — the
data under /data, the default books and resource workbooks the app ships under /app
(read-only) — and the entrypoint refuses one that points elsewhere; no .env file is read
there. The values are listed in Run with Docker.
Script-only switches, documented in their scripts and not in .env.example:
PTX_VERIFY (set by verify_db_copy.py), PTX_WIND_SRC / PTX_SOLAR_SRC /
PTX_RESOURCE_SRC (the private ingest scripts’ source-file overrides).
Not read, and no longer declared: PTX_SOLVER, PTX_SOLVER_TIMEOUT (the solver
is HiGHS, chosen in core/feasibility.py) and PTX_API_HOST / PTX_API_PORT /
PTX_API_RELOAD (host, port and reload come from the uvicorn command line). An
older .env that still sets them is harmless; they are ignored.
Where the cache of a confidential series lands
A dataset flagged confidential caches its Parquet under
<private>/processed/, never in the shared processed/site_datasets/. A cache
file written to the shared location before this split is still read from
there (left where it is, never copied); a fresh one is written private.
Practical implications
- To move all the data: set
PTX_DATA_ROOTand copy the tree there (the DAB folder included — the shipped books are tracked in git underbackend/data/dab, the root’sdab/must hold them or the default book cannot be read — anddab_workspaces/, each workspace’s own books, which exist nowhere else). - A copy of the app on other ports (verification): the API refuses pages it was not
told about, so start it with
PTX_DEV_ORIGINS=http://localhost:<the copy page's port>. - To keep the database elsewhere: set
PTX_DB_PATHalone; results and caches stay under the root. - To change host/port: pass them to
uvicorn(--host,--port,--reload). - Importing
backend.api.appno longer creates the database: the API object is built on the first request, so a tool that imports the module leaves no file behind. - Deleting a project removes its whole
results/<pid>/folder — every store’s files, thebook_impact_*.jsondocuments, and finally the empty folder. A file no store claims keeps the folder (never a blind delete).
How to modify this safely
- A new path goes into
paths.pyas a function that reads the environment when called — never a module constant;test_paths.pyfails if a builder keeps an import-time copy, and if.env.exampleor this page misses a variable the code reads (or declares one it does not). - Never put secrets in
.envfor this tool: passwords are stored hashed in the database and a session lives in an HttpOnly cookie; nothing secret is configured.
Related topics
- Getting started
- Data model
- Known gaps — internal,
docs/08-maintenance/known-gaps.md
From the app’s documentation, docs/05-development/environment-variables.md · Hyprisma 1.0.5