Files
0pt/.env.example
admin dade37ee05
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m58s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m25s
feat(credits): one-time trial grant so the ledger runs in production
- Ledger accepts kind='grant' (a server-owned balance grant, not a payment).
- OPT_TRIAL_CREDITS=N grants N credits once per tenant (idempotent key
  trial:<tenant>) on balance/structure, so reserve -> settle is exercised
  before any customer purchase.
- sql/020 extends the kind constraint (re-runnable).
- Tests for the grant helper; full suite 1345 passed.
2026-11-02 22:05:06 +01:00

292 lines
14 KiB
Bash

# =============================================================================
# 0pt (edge plane) -- environment inventory / example environment
# =============================================================================
# Copy to `.env` (gitignored) and fill in real values. Never commit real values.
# Vercel env for the edge app; VPS .env (git-ignored) for infra/docker-compose.yml.
# See docs/08-infrastructure.md for the canonical description of each integration.
#
# Two provenance markers are used below and they mean different things:
# * "wired in code today" -- read via a `process.env.*` somewhere in
# this repo right now (grep-verified).
# * "documented target, not yet read" -- named in docs/08-infrastructure.md /
# docs/02-architecture.md but with no
# `process.env.*` read yet. Filling it in
# unblocks the integration; it does not by
# itself change behaviour.
#
# The app is designed to build and boot with all of these UNSET: lib/db.ts and
# lib/stripe.ts are lazy + env-guarded, and the storage/payment/doc routes return
# HTTP 501 ("not configured") until the relevant keys are provided.
# ---------------------------------------------------------------------------
# Core / public
# ---------------------------------------------------------------------------
# Public base URL of this app (used for absolute links / callbacks).
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Documented target (docs/08); no call site found reading it in this pass.
# NOTE: api.0pt.app is a dead hostname (falls through to wildcard Vercel ->
# DEPLOYMENT_NOT_FOUND). The app serves its own /api routes on the site
# origin, so the honest base URL is the live origin itself.
NEXT_PUBLIC_API_URL=https://0pt.app
# ---------------------------------------------------------------------------
# Database (Postgres) -- wired in code today: lib/db.ts, app/api/health
# ---------------------------------------------------------------------------
# Vercel Postgres sets POSTGRES_URL / POSTGRES_PRISMA_URL automatically when the
# integration is attached. First one set wins, in this order (lib/db.ts):
# DATABASE_URL -> POSTGRES_URL -> POSTGRES_PRISMA_URL.
DATABASE_URL=postgres://user:password@localhost:5432/opt
POSTGRES_URL=
POSTGRES_PRISMA_URL=
# ---------------------------------------------------------------------------
# Cache / queue (Redis) -- app/api/health only checks presence; no other call
# site found reading it in this pass (worker queue usage, if any, lives in
# services/api, outside this repo's scope).
# ---------------------------------------------------------------------------
REDIS_URL=redis://localhost:6379
# ---------------------------------------------------------------------------
# Auth -- session / dev-login / service-token secrets (wired in code today)
# ---------------------------------------------------------------------------
# Session-signing secret (generate a long random string).
AUTH_SECRET=change-me-long-random-string
# AUTH_URL is documented (docs/08) but not yet read by any route in this pass.
AUTH_URL=https://0pt.app
# Optional dev-only login shortcut key (leave UNSET in production).
DEV_LOGIN_KEY=
# Shared token for internal/service-to-service calls; sent as X-Internal-Token
# on every engineFetch() call (lib/engine.ts).
INTERNAL_TOKEN=change-me-internal-token
# ---------------------------------------------------------------------------
# Zitadel / OIDC Bearer verification (Wave 2 hardening) -- wired in code today
# ---------------------------------------------------------------------------
# 0pt verifies inbound Bearer tokens against the fleet Zitadel IdP
# (src/auth/zitadel.ts, wired via middleware.ts). JWKS are discovered from
# <issuer>/.well-known/openid-configuration, falling back to <issuer>/oauth/v2/keys.
#
# Fleet Zitadel issuer (OIDC). Default when unset: https://id-customers.siax.io
ZITADEL_ISSUER=https://id-customers.siax.io
# Browser login (Authorization Code + PKCE). Create a Web application in the
# Zitadel project with redirect URI https://0pt.app/api/auth/callback.
ZITADEL_OIDC_CLIENT_ID=
ZITADEL_OIDC_CLIENT_SECRET=
# Enforcement is FAIL-CLOSED. Leave this UNSET (or set it to "true") and a
# valid, verified Bearer token is REQUIRED on /api/0pt/* -- 401 on
# missing/invalid, 403 when X-Tenant-Id disagrees with the token tenant.
# ONLY the literal value "false" disables authentication, and that value is
# REFUSED when NODE_ENV=production: the app throws at startup instead of
# serving openly. Use "false" for local development only.
AUTH_ENFORCE=true
# Optional override for the claim key that carries the SIAX tenant id. When
# unset, tenant derivation uses the documented precedence:
# urn:zitadel:iam:org:id -> urn:zitadel:iam:user:resourceowner:id -> tenant_id -> tenant.
ZITADEL_TENANT_CLAIM=
# ---------------------------------------------------------------------------
# Data engine bridge -- wired in code today: lib/engine.ts
# ---------------------------------------------------------------------------
# When set, edge route handlers defer to the FastAPI engine (services/api) as the
# single source of truth (orders/estimates/catalog/ai-parse). When unset, the edge
# computes locally / falls back to an honest heuristic -- see lib/engine.ts and
# app/api/ai/parse.
ENGINE_URL=http://localhost:8000
# ---------------------------------------------------------------------------
# SIAX platform clients
# ---------------------------------------------------------------------------
# WIRED FOR REAL since 2026-08-31: every factory in src/clients/* reads its
# *_URL from process.env when no constructor baseUrl is given. Under
# NODE_ENV=production an UNSET url is a hard boot failure
# (SiaxError DEPENDENCY_UNAVAILABLE) — production never silently falls back
# to the in-memory mocks (Master-DoD §3/§10). In dev/test, unset = documented
# local mock/no-op behaviour.
CL0UD_URL=
N0D_URL=
AUD0_URL=
# Optional service bearer tokens for the platform clients (machine identity).
# Values come from Infisical/secret refs — NEVER commit real values here.
CL0UD_SERVICE_TOKEN=
N0D_SERVICE_TOKEN=
AUD0_SERVICE_TOKEN=
ST0RE_SERVICE_TOKEN=
# INF0 (AI OS) -- documented target, not yet read by any code in this repo.
# Route AI-parsning / model calls through the gateway instead of raw provider keys.
INF0_URL=
# ST0RE_URL is the convention name for src/clients/st0re-client.ts /
# st0re-lakehouse-client.ts and is read by the real-infra test suite
# (tests/p0/*.realinfra.test.ts). The lakehouse write path was live-verified
# against ST0RE on 2026-07-13 with this set; unset, the clients fall back to
# their deterministic in-memory mocks.
ST0RE_URL=
# ---------------------------------------------------------------------------
# P0 Data-OS backends (src/search, src/analytics, src/metadata, src/features)
# ---------------------------------------------------------------------------
# OpenSearch — search projections ONLY, never canonical truth (§42). Unset in
# dev/test = search capability unavailable (typed error), never a fake.
OPENSEARCH_URL=
# ClickHouse — 0pt capability analytics (tenant-isolated metric events).
# HAS NO CONNECTION to b0ts pipelines or quarantined market data (legal).
CLICKHOUSE_URL=
# "user:password" for ClickHouse basic auth; from secret refs only.
CLICKHOUSE_AUTH=
# OpenMetadata — metadata PROJECTION target (canonical stays in 0pt).
OPENMETADATA_URL=
OPENMETADATA_TOKEN=
# Feast feature server (online features path).
FEAST_SERVER_URL=
# ---------------------------------------------------------------------------
# Payments (Stripe) -- wired in code today: app/api/payments/*,
# app/api/webhooks/stripe
# ---------------------------------------------------------------------------
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# Documented target (docs/08); not yet read by any route in this pass.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
# ---------------------------------------------------------------------------
# Object storage -- Cloudflare R2 / S3-compatible
# ---------------------------------------------------------------------------
# R2_ENDPOINT / R2_BUCKET / R2_ACCESS_KEY_ID are read in code today; all three
# are required to enable /api/uploads, otherwise it returns 501.
# R2_ACCOUNT_ID and R2_SECRET_ACCESS_KEY are documented in docs/08 but were NOT
# found via a process.env.* read in this pass -- verify before assuming the R2
# client is fully wired.
R2_ENDPOINT=https://<accountid>.r2.cloudflarestorage.com
R2_BUCKET=opt-uploads
R2_ACCESS_KEY_ID=change-me
R2_ACCOUNT_ID=
R2_SECRET_ACCESS_KEY=
# Fallback local artifact store used when no object storage is configured
# (src/adapters/local-blob-store.ts). Leave unset in production.
OPT_LOCAL_BLOB_DIR=./.opt-blobs
# ---------------------------------------------------------------------------
# Document intelligence / OCR (Azure) -- wired in code today:
# src/services/document-extraction-service.ts. Unset => extraction
# "not configured".
# ---------------------------------------------------------------------------
DOC_INTELLIGENCE_ENDPOINT=https://<resource>.cognitiveservices.azure.com
DOC_INTELLIGENCE_KEY=change-me
DOC_INTELLIGENCE_API_VERSION=2024-11-30
# ---------------------------------------------------------------------------
# Customer DB-grant encryption -- wired in code today:
# /api/connections/db-grant
# ---------------------------------------------------------------------------
# 32-byte key, AES-GCM. Encrypts customer-supplied read-only connection strings
# at rest.
GRANT_ENC_KEY=change-me-32-byte-key
# ---------------------------------------------------------------------------
# External write-back targets
# ---------------------------------------------------------------------------
# Odoo base URL for the bills/write-back adapter.
ODOO_BASE_URL=https://<your-odoo-host>
# ---------------------------------------------------------------------------
# Email -- Resend (documented target, docs/08; no Resend call site found in
# this pass -- treat as not-yet-implemented, not just unconfigured)
# ---------------------------------------------------------------------------
RESEND_API_KEY=
# ---------------------------------------------------------------------------
# Embedded analytics (documented target, docs/08; no call site found in this
# pass)
# ---------------------------------------------------------------------------
METABASE_SITE_URL=
METABASE_SECRET_KEY=
NOCODB_BASE_URL=
NOCODB_API_TOKEN=
# ---------------------------------------------------------------------------
# Test-only (tests/p0 real-infra suite; not needed to run the app)
# ---------------------------------------------------------------------------
# ST0RE_TEST_TENANT_ID=
# ---------------------------------------------------------------------------
# Document workbench (upload -> DOC0 -> INF0 -> validated export -> credits)
# Wired in code today: app/api/0pt/documents/*, app/api/0pt/credits/*.
# Every value is optional to boot and mandatory to USE: a missing integration
# makes its route answer 503 "not configured" instead of fabricating success.
# ---------------------------------------------------------------------------
# DOC0 document service. URL must be absolute HTTPS; token is a service PAT.
DOC0_URL=https://doc0.siax.io
DOC0_SERVICE_TOKEN=
# The DOC0 service identity's own actor id (token subject). DOC0 rejects an
# x-actor-id it has no actor_grant for; 0pt keeps the end-user actor in its own
# journal and lets the DOC0 call act as the service.
DOC0_SERVICE_ACTOR_ID=
# Maximum accepted upload size (bytes). Default 25 MiB.
OPT_MAX_UPLOAD_BYTES=26214400
# Interim AI path: self-hosted Ollama on the SIAX decision server
# (server-decision-01). Internal only; no provider key and no per-token cost.
# Plain HTTP is accepted only for loopback/RFC1918/tailnet hosts.
DECISION_OLLAMA_URL=http://100.96.133.104:11434
DECISION_OLLAMA_MODEL=hermes-qwen3.5:9b
# INF0 AI gateway (model calls). No provider key is ever placed in this repo.
# Live path today: the LiteLLM gateway (OpenAI-compatible). The native
# /v1/inf0/* surface is contract-only; INF0_URL is used as a fallback.
INF0_GATEWAY_URL=https://ai-gateway.siax.io/v1
INF0_GATEWAY_LITELLM_KEY=
INF0_GATEWAY_PROVIDER_KIND=external
INF0_URL=https://api.inf0.siax.io
INF0_API_TOKEN=
# PAY0 (optional; Stripe is used directly for credit checkout today).
PAY0_URL=https://pay0.siax.io
PAY0_API_TOKEN=
# N0D / ST0RE (used by the broader pipeline; not required by the routes above).
N0D_URL=https://api.n0d.siax.io
N0D_API_TOKEN=
ST0RE_URL=https://api.st0re.siax.io
ST0RE_API_TOKEN=
# OpenFGA project authorization. Without all four values, document routes fail
# closed with PROJECT_AUTHORIZATION_NOT_CONFIGURED.
OPENFGA_API_URL=https://fga.siax.io
OPENFGA_STORE_ID=
OPENFGA_MODEL_ID=
OPENFGA_AUTH_TOKEN=
# Transitional, server-owned project grants used only when OpenFGA is not
# configured. JSON array; actorId "*" means every verified actor in the tenant.
# OPT_PROJECT_GRANTS=[{"tenantId":"379976258701823235","projectId":"prj_demo","actorId":"*"}]
OPT_PROJECT_GRANTS=
# Model policy for the document workbench. Without this, structuring answers
# MODEL_POLICY_NOT_CONFIGURED.
OPT_ALLOWED_MODELS=
OPT_ALLOWED_DATA_CLASSES=none,low
OPT_MODEL_DESTINATION=external
OPT_EXTERNAL_TRANSFER_ALLOWED=false
OPT_MODEL_MAX_INPUT_TOKENS=60000
OPT_MODEL_MAX_OUTPUT_TOKENS=4000
# Server-owned, versioned credit price book (JSON, one line). No price is ever
# read from a browser. Without it, credit packs/checkout/structuring answer
# PRICE_BOOK_NOT_CONFIGURED.
# {"version":"2026-11-01","currency":"SEK","packs":[{"id":"starter","credits":100,"amountMinor":9900}],"metering":{"pagesPerCredit":5,"inputTokensPerCredit":20000,"outputTokensPerCredit":4000}}
OPT_CREDIT_PRICE_BOOK=
# Pilot switch. `off` runs document structuring without a wallet; it is
# explicit (never a silent fallback), the response reports `credits: null`, and
# purchases stay disabled. Unset/`on` requires the price book + ledger.
OPT_CREDITS_MODE=
# One-time trial credits granted per tenant (server-owned, idempotent, NOT a
# payment). 0 disables. With OPT_CREDITS_MODE=on this lets reserve/settle run
# in production before any customer purchase.
OPT_TRIAL_CREDITS=0
# Central Keycloak (SIAX Portal)
KEYCLOAK_ISSUER=https://auth.portal.siax.dev/realms/portal
KEYCLOAK_EXPECTED_AUDIENCE=account
KEYCLOAK_AUTHORIZED_PARTY=siax-portal