Skip to content

Environment Variables

Every EcoCtrl process reads its configuration from a .env.local file co-located with its source. .env.local always takes precedence over .env.example, which serves as a template and a fallback for missing keys.

Server (packages/server/.env.local)

Loaded by dotenv at the top of packages/server/index.ts.

Core

VariableRequiredDefaultDescription
DATABASE_URLyesPostgreSQL connection string, e.g. postgresql://ecoctrl:ecoctrl_secret@localhost:5432/ecoctrl. The server attempts CREATE DATABASE on first boot if the role has the privilege.
JWT_SECRETyesUsed to sign access tokens. Changing it invalidates every issued token.
PORTno3000Listening port.
HOSTno0.0.0.0Listening host.
CORS_ORIGINnoreflect anyComma-separated allowlist (e.g. https://app.example.com,https://admin.example.com). When unset, the server reflects the request origin.

Mail (verification codes)

The platform stores SMTP credentials in the platform_configs row; on boot, syncSmtpFromEnv() overwrites that row with whatever is in env. Set these once and they propagate.

VariableDescription
SMTP_HOSTSMTP relay host.
SMTP_PORTSMTP port (default 587).
SMTP_USERSMTP username.
SMTP_PASSSMTP password / app password.
SMTP_SECUREtrue for SMTPS (port 465), false for STARTTLS.

IoT gateway proxy (optional)

VariableDescription
BASE_URLUpstream IoT gateway base URL.
APP_IDUpstream gateway application id used during token refresh.

Weather widget (optional)

VariableDefaultDescription
OPENWEATHER_API_KEYOpenWeatherMap API key. When empty, the weather card on the dashboard is hidden.
WEATHER_LAT39.9042Default latitude.
WEATHER_LNG116.4074Default longitude.
WEATHER_LOCATIONBeijingDisplay name.

OAuth providers (optional)

Configured providers are returned by GET /api/auth/oauth/providers; if the list is empty the admin UI hides the OAuth buttons.

VariableDescription
WECHAT_APPID, WECHAT_SECRETEnable WeChat login.
FEISHU_APPID, FEISHU_SECRETEnable Feishu login.

Admin & web (apps/{admin,web}/.env.local)

These variables are read by the runtime layer (Caddy in Docker, lws in the release zip), not by the JavaScript bundle. Client code always issues requests against the literal /api and /static prefixes.

VariableDefaultDescription
API_BASE_URLhttp://localhost:3000Upstream API host. Inside Docker compose use the service name (http://server:3000).
API_PREFIX/apiPath prefix on the upstream that handles JSON requests.
STATIC_PREFIX/staticPath prefix on the upstream that serves uploaded files (3D models).

Why no VITE_* variables here?

Putting the API host into the bundle means rebuilding for every deployment target. EcoCtrl chose to keep the API host out of the bundle entirely; only the proxy layer needs to know it. See Architecture for how the rewrite happens in each environment.

Docker Compose (docker/.env.local)

docker/compose.yml interpolates these into the server service environment:

VariableDescription
JWT_SECRETRequired. Mounted into the server container.
BASE_URLOptional. Forwarded to the server's BASE_URL.
APP_IDOptional. Forwarded to the server's APP_ID.

Database credentials, ports and DATABASE_URL are hardcoded inside compose.yml so the stack works out of the box. Edit the compose file if you need different credentials or different ports.

Order of precedence

When the same variable is set in multiple places, EcoCtrl resolves it in this order (highest wins):

  1. Shell environmentJWT_SECRET=... ./start.sh.
  2. Per-app .env.localapps/admin/.env.local, apps/web/.env.local, packages/server/.env.local.
  3. Per-app .env.example — used by tests / fallbacks only.
  4. Built-in defaults in start.sh and inside the server.

The release zip's start.sh additionally honors ROOT/.env.local as a shared fallback for both admin and web, useful when both apps point to the same backend.

Released under the MIT License.