NODE_ENV=development PORT=4000 POSTGRES_PORT=5432 DATABASE_URL=postgres://postgres:postgres@localhost:5432/fshd_openrd # DB SSL is gated SOLELY on this flag — NOT forced by NODE_ENV. A # REMOTE / managed DB (RDS, Cloud SQL, …) MUST set it true (and # configure DATABASE_SSL_REJECT_UNAUTHORIZED as appropriate). For a # compose-internal / private-network Postgres that speaks no SSL, # leave it false AND set DATABASE_ALLOW_INSECURE=true below. DATABASE_SSL_ENABLED=false DATABASE_SSL_REJECT_UNAUTHORIZED=true # Explicit ack that an unencrypted prod DB connection is intentional. # Prod fail-fasts when SSL is off and this is not set — so a remote DB # that forgot DATABASE_SSL_ENABLED can't silently send PHI in cleartext. # Set true ONLY for a compose-internal / private-network Postgres. DATABASE_ALLOW_INSECURE=false JWT_SECRET=change-me-super-secret JWT_EXPIRES_IN=7d BCRYPT_SALT_ROUNDS=10 CORS_ORIGIN=http://localhost:8081,http://localhost:8080 LOG_LEVEL=debug # OTP OTP_PROVIDER=mock OTP_CODE_LENGTH=6 OTP_TTL_MINUTES=10 OTP_RESEND_INTERVAL_SECONDS=60 OTP_MAX_SEND_PER_DAY=10 OTP_MAX_VERIFY_ATTEMPTS=5 OTP_HASH_SECRET=change-me-otp-secret # Internal-test OTP bridge — set OTP_PROVIDER=internal_test to use it. # Temporary until Tencent SMS is wired (TencentOtpProvider is a stub). # Only these allowlisted numbers can log in, using OTP_TEST_FIXED_CODE; # every other number is rejected (403). In prod BOTH must be set or # startup fails (allowlist non-empty, code = OTP_CODE_LENGTH digits). # Numbers may be written bare (13922220001) OR with the country code # (+8613922220001), comma-separated — both normalize to the same +86 # form the mobile app actually sends, so either matches a real login. # Switch OTP_PROVIDER=tencent once real SMS lands. OTP_TEST_PHONE_ALLOWLIST= OTP_TEST_FIXED_CODE= # Tencent Cloud SMS — set OTP_PROVIDER=tencent for real SMS delivery. # All five below are REQUIRED when the provider is tencent (prod # fail-fasts naming any that are missing). The approved SMS template # MUST have exactly two variables — {1}=verification code, {2}=TTL # minutes — matching what TencentOtpProvider sends, e.g.: # 您的验证码{1},请于{2}分钟内填写,如非本人操作请忽略本短信。 # Credentials come from the Tencent Cloud SMS console (SecretId/Key from # 访问管理 CAM; SdkAppId/SignName/TemplateId from 短信控制台). TENCENT_SECRET_ID= TENCENT_SECRET_KEY= TENCENT_SMS_SDK_APP_ID= TENCENT_SMS_SIGN_NAME= TENCENT_SMS_TEMPLATE_ID= TENCENT_SMS_REGION=ap-guangzhou AUTH_RATE_LIMIT_WINDOW_SECONDS=60 AUTH_RATE_LIMIT_MAX_REQUESTS=20 OTP_SEND_RATE_LIMIT_WINDOW_SECONDS=60 OTP_SEND_RATE_LIMIT_MAX_REQUESTS=5 LOGIN_MAX_FAILURES=5 LOGIN_LOCK_MINUTES=15 # AI (Node API) # Note: AI_API_MODEL here must match `env.ts`'s default # (`deepseek-ai/DeepSeek-V3`) — if a user copies `.env.example` → # `.env` and removes this line, the runtime falls back to env.ts's # default, NOT what they thought they were running. Keep these two # in lockstep when changing the default model. # # ⚠️ SiliconFlow runs TWO independent platforms — .cn (国内站) and .com # (international). Accounts, API keys, balances, and model catalogs do # NOT carry across: a key minted on cloud.siliconflow.COM returns 401 # "Api key is invalid" against api.siliconflow.CN, and vice-versa. Match # this URL to the console your key came from — cloud.siliconflow.cn → # keep .cn below; cloud.siliconflow.com → switch host to # api.siliconflow.com AND pick AI_API_MODEL from the .com catalog (the # model IDs differ too, e.g. .com's zai-org/GLM-5.2 vs .cn's # deepseek-ai/DeepSeek-V3). AI_API_BASE_URL=https://api.siliconflow.cn/v1 AI_API_MODEL=deepseek-ai/DeepSeek-V3 AI_API_TIMEOUT=30000 OPENAI_API_KEY= AI_API_KEY= AI_RATE_LIMIT_WINDOW_SECONDS=60 AI_RATE_LIMIT_MAX_REQUESTS=6 AI_PROGRESS_RATE_LIMIT_WINDOW_SECONDS=60 AI_PROGRESS_RATE_LIMIT_MAX_REQUESTS=30 # OCR (Baidu) BAIDU_OCR_API_KEY= BAIDU_OCR_SECRET_KEY= BAIDU_OCR_GENERAL_ENDPOINT=https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic BAIDU_OCR_ACCURATE_ENDPOINT=https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic BAIDU_OCR_MEDICAL_ENDPOINT=https://aip.baidubce.com/rest/2.0/ocr/v1/medical_report # Integrated report OCR / parser OCR_PROVIDER=embedded OCR_PYTHON_BIN=python3 OCR_PARSER_TIMEOUT_MS=120000 OCR_DISABLE_PADDLE=true # File storage STORAGE_PROVIDER=local MINIO_ENDPOINT=minio:9000 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin12345678 MINIO_BUCKET_NAME=medical-reports MINIO_USE_HTTPS=false # Chroma Cloud # # As of v2.4.0 the KB lives in local pgvector and the API no longer # reads Chroma at runtime. The vars below are only required by the # one-shot migration / re-ingest scripts in `scripts/`: # - kb-import-from-chroma.py — pulls historical corpus from Chroma # Cloud and re-embeds into pgvector # - kb-ingest.* — accepts `--dry-run --source chroma` for spot # checks # Leave empty / placeholder if you don't need to touch Chroma. CHROMA_API_KEY=your_chromadb_cloud_api_key_here CHROMA_TENANT_ID=your_tenant_id_here CHROMA_DATABASE=FSHD CHROMA_COLLECTION=fshd_knowledge_base CHROMA_API_HOST=localhost CHROMA_API_PORT=5000 # Knowledge service KB_SERVICE_HOST=127.0.0.1 KB_SERVICE_PORT=5010 KB_SERVICE_URL= # Shared bearer token between the Node API and the Python KB service. # Both sides MUST see the same value (docker-compose wires both # containers from this single env). The KB service refuses to start # when KB_SERVICE_HOST is 0.0.0.0 (the docker-compose case) or # NODE_ENV=production without this set, so a non-loopback bind can # never accidentally expose the KB to anonymous callers. # # Generate a real value with one of: # openssl rand -hex 32 # python -c "import secrets; print(secrets.token_hex(32))" # # Required in staging / production. May be left empty for plain # loopback dev where the Node API runs natively (no docker) and the # KB service binds to 127.0.0.1. When using `docker compose up` the # compose file falls back to a `dev-only-local-token-NOT-FOR-PROD` # placeholder so a fresh clone works without touching this — but you # can override here to test the real auth path locally. KB_SERVICE_TOKEN= KB_READY_WAIT_SECONDS=90 HEALTHCHECK_TIMEOUT_MS=2500 # Override only when the Docker kb-service should read from a Postgres # instance other than the compose `postgres` service — typically when # you ingested into native pg and want the container to reach it via # host.docker.internal. See docs/proposals/local-rag-migration.md §4.4. KB_SERVICE_DATABASE_URL= # Knowledge base backend # Switch between the local pgvector store (default) and Chroma Cloud # (kept as a one-flag fallback). See docs/proposals/local-rag-migration.md. KB_BACKEND=pgvector KB_EMBED_MODEL=BAAI/bge-m3 # HuggingFace endpoint for downloading the embedding model on first # boot. Leave this COMMENTED (= unset) to use the default # huggingface.co — do NOT set it to an empty value, huggingface_hub # treats "" as a broken host-less endpoint. In mainland China # huggingface.co is blocked, so uncomment the mirror line below, or # kb-service can't fetch bge-m3 and stays unhealthy ("Network is # unreachable"): # HF_ENDPOINT=https://hf-mirror.com KB_INGEST_BATCH_SIZE=32 # Connection pool sizing for the pgvector backend. Defaults (1, 2) fit # the current single-process KB service; raise the max when adding # workers. KB_PG_POOL_MIN=1 KB_PG_POOL_MAX=2 # GitHub App reviewer (see docs/github-app-reviewer.md) # `npm run github:app-token` / `npm run github:app-pr-review` will read # these via Node's --env-file-if-exists=.env. PEM file lives outside the # repo (typically ~/.config/openrd/review-bot.pem with chmod 600). GITHUB_APP_ID= GITHUB_APP_PRIVATE_KEY_PATH= GITHUB_REPOSITORY=OpenRareDisease/openrd # Optional. The scripts resolve installation id from --repo when this # is unset, but setting it skips one round-trip to GitHub. Find it # under https://github.com/settings/installations/ after # installing the App. GITHUB_APP_INSTALLATION_ID= # Reverse proxy / HTTPS (prod only) # The `caddy` service (docker-compose `prod` profile) terminates TLS # for this domain and auto-obtains + renews a Let's Encrypt cert. Read # by Caddyfile as the site address. Local dev leaves it unset — caddy # isn't started without `docker compose --profile prod`. DOMAIN=ai.fshdyouth.com # Mobile (Expo) # Dev points the app straight at the local API. PROD must point the # mobile app at the public HTTPS origin so traffic goes through Caddy: # EXPO_PUBLIC_API_URL=https://ai.fshdyouth.com/api # (rebuild the app after changing). The web build uses a same-origin # relative path, so WEB_EXPO_PUBLIC_API_URL=/api is correct everywhere. EXPO_PUBLIC_API_URL=http://localhost:4000/api WEB_EXPO_PUBLIC_API_URL=/api