Files
accounted/.env.example
T
Mattsson d8cf78330e docs(self-hosting): own-credentials section, stale connector lines, complete .env.example (#2146)
* docs(self-hosting): own-credentials section, stale connector lines, complete .env.example (#2131)

SELF-HOSTING.md said the Skatteverket client wiring "ships in a following
release"; PR #2103 merged it, so both bank sync and Skatteverket now carry
traffic through the hosted proxy with a key. The two stale sentences are
replaced and SOVEREIGN.md line 48 says the same thing.

New "Own credentials (no connector key)" subsection documents the path an
operator takes without a key: Enable Banking app in restricted production
mode with the callback URL, the Skatteverket developer-portal application
with the redirect URI, every variable the code reads, the five production
base URLs (all defaults point at the test environment), the kill switch,
and the rule that any own credential switches that upstream out of
connector mode.

.env.example gains the Skatteverket block, the optional Enable Banking
variables, and RESEND_INBOUND_DOMAIN / RESEND_INBOUND_WEBHOOK_SECRET, which
the invoice-inbox manifest requires but the example never listed.
DOCKER.md no longer claims Enable Banking is excluded from the self-host
preset (docker/extensions.self-hosted.json ships it).

ENABLE_BANKING_SANDBOX is removed from the enable-banking manifest and the
index.ts header: declared as optional, never read anywhere; the sandbox is
selected by ENABLE_BANKING_API_URL. Logged in DECISIONS.md.

Closes #2131

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6M2ZoZc6QDRxU3m9WzgE
Signed-off-by: Emil <emilmattsson14@gmail.com>

* docs(self-hosting): correct key format, AISP scope caveat, SKV scopes and rotation note (#2131)

Skeptic findings on PR #2146, one pass:
- ENABLE_BANKING_PRIVATE_KEY: the decoder base64-decodes first and wraps
  anything else as DER, so a raw PEM fails at JWT signing. The docs and
  .env.example no longer claim it is accepted.
- Enable Banking restricted mode covers the operator's own accounts only;
  an instance hosting client companies is doing licensed AIS and needs
  the connector key or its own AISP registration. Said so.
- Listed the OAuth scopes the app requests (both AGI scopes), noted that
  the kill switch gates API calls, not the BankID login, and that the
  token encryption key has no dual-key rotation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6M2ZoZc6QDRxU3m9WzgE
Signed-off-by: Emil <emilmattsson14@gmail.com>

---------

Signed-off-by: Emil <emilmattsson14@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-01 23:08:47 +02:00

278 lines
16 KiB
Bash

# Local development environment variables.
# Copy to .env and fill in the values: cp .env.example .env
# ── Required ──────────────────────────────────────────────
# Supabase project credentials (Dashboard -> Settings -> API)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-or-publishable-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-or-secret-key
# App base URL (local dev)
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Shared hosted white-label deployments only: exact comma-separated hostnames
# that are registered on this deployment. No wildcards. Invite and auth links
# use a listed request/browser host; every other host falls back to
# NEXT_PUBLIC_APP_URL. Also add each listed host's /auth/callback and /invite/*
# URLs to the Supabase Auth redirect allowlist before deploying it.
# NEXT_PUBLIC_WHITELABEL_DOMAINS=portal.brand-one.example,books.brand-two.example
# Secret for authenticating cron/scheduled requests.
# Any non-empty random string for local dev: openssl rand -hex 16
CRON_SECRET=generate-a-random-secret
# Receipt hunt (nightly matcher, 05:30 UTC): comma-separated company ids the
# hunt may stage proposals for. Unset means it runs for nobody, so enabling it
# is always a deliberate act rather than a side effect of deploying.
# Model used to resolve bank descriptors to merchants and to decide which mail
# is the receipt for which purchase. Falls back to BEDROCK_MODEL_ID.
RECEIPT_HUNT_MODEL_ID=
# Floor for accepting the model's pairing. Every proposal is human-reviewed, so
# this trades recall against review effort, not against correctness.
RECEIPT_HUNT_MIN_CONFIDENCE=
RECEIPT_HUNT_COMPANY_IDS=
# Session timeouts are opt-in per user (user_preferences.auto_logout, toggled
# in Settings > Security): users who have not opted in stay signed in for the
# full Supabase session lifetime. The variables below set the limits that
# apply to opted-in users: 30 minutes idle, 12 hours absolute, warning 2
# minutes before expiry. Set a timeout to 0 to disable that limit entirely.
# Self-hosted deployments default both limits to 0 unless overridden.
# NEXT_PUBLIC_SESSION_TIMEOUT_FORCE_ALL=true enforces the timeouts for every
# user regardless of their preference (emergency lever / strict deployments).
# The signing key falls back to SUPABASE_SERVICE_ROLE_KEY; set a dedicated
# random secret if session signing should rotate independently.
# NEXT_PUBLIC_SESSION_IDLE_TIMEOUT_MS=1800000
# NEXT_PUBLIC_SESSION_ABSOLUTE_TIMEOUT_MS=43200000
# NEXT_PUBLIC_SESSION_WARNING_MS=120000
# NEXT_PUBLIC_SESSION_TIMEOUT_FORCE_ALL=false
# SESSION_TIMEOUT_SECRET=
# Self-hosted only: set to true when public signup is turned off in your
# GoTrue/Supabase auth config (GOTRUE_DISABLE_SIGNUP / "Allow new users to
# sign up" off). GoTrue offers no clean server-side read of that setting, so
# this flag mirrors it. When true, inviting a teammate who has no account
# provisions the account server-side via the auth admin invite API (GoTrue
# must have SMTP configured to deliver that mail) instead of relying on
# public /register, which GoTrue would reject with "Signups not allowed".
# The GoTrue redirect URI allow-list (URI Allow List / GOTRUE_URI_ALLOW_LIST)
# must include /invite/* or the invite email's redirect silently falls back
# to SITE_URL.
# Hosted keeps this unset: public signup stays open there.
# AUTH_SIGNUPS_DISABLED=false
# Sign in with Google. Requires the Google provider to be configured in
# Supabase/GoTrue first (Google Cloud OAuth client + redirect URI):
# https://supabase.com/docs/guides/auth/social-login/auth-google
# The button stays hidden until this is true.
# NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=true
# Cloudflare Turnstile site key for Supabase Auth bot protection. This value is
# public and is embedded in the browser bundle. Leave it unset until a widget
# has been created for the deployment's exact hostnames. Deploy the site key
# before enabling Turnstile with the matching SECRET in Supabase Auth, so the
# existing login flow remains available throughout rollout.
# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
# ── Optional: extension features (core runs without these) ─
# AI features (document extraction + AI assistant). Three ways to provide a
# backend; set one of them. AI_PROVIDER (bedrock|anthropic|openai-compatible)
# forces the choice if several are present; otherwise Bedrock wins, then the
# direct Anthropic API, then an OpenAI-compatible endpoint.
#
# 1. Claude via AWS Bedrock. Needs an AWS account with Bedrock model access to
# Claude. Keeps inference in eu-north-1, which is what hosted runs.
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_REGION=eu-north-1
#
# 2. Claude via the direct Anthropic API. No AWS account needed. Note that it
# has no EU-residency guarantee: use Bedrock if you need one.
# ANTHROPIC_API_KEY=
#
# 3. Any OpenAI-compatible endpoint (chat-completions API), e.g. a Swedish
# inference provider for a sovereign self-host. Document extraction and
# single-call AI jobs run here; the in-app chat assistant does not yet.
# A model id is required (no default exists for an arbitrary endpoint).
# AI_BASE_URL=https://api.example.se/v1
# AI_API_KEY=
# AI_MODEL= # default model for every tier
# AI_EXTRACTION_MODEL= # per-tier overrides (also AI_ASSISTANT_MODEL, AI_HEAVY_MODEL);
# # legacy BEDROCK_MODEL_ID / BEDROCK_SONNET_MODEL_ID /
# # BEDROCK_OPUS_MODEL_ID keep working as the same overrides
# AI_VISION=true # openai-compatible only: set false for a text-only model
# # (images/PDFs are then skipped honestly; HTML mail still extracts)
# AI_PDF_MODE=auto # auto (Claude: native, others: rasterize with poppler) | native | rasterize
# AI_PDF_MAX_PAGES=4 # pages rasterized per PDF
# AI_PDF_RASTERIZER_BIN=pdftoppm # poppler binary name/path (the self-host image installs poppler-utils)
# AI_STRICT_JSON=false # openai-compatible only: response_format json_schema when the provider enforces it
# AI_EXTRACTION_MAX_TOKENS=8192 # output cap for document extraction (legacy BEDROCK_MAX_TOKENS)
# AI_PROVIDER=
# Self-hosted connector subscription (instance side). Leave unset on hosted.
# GNUBOK_CONNECTOR_KEY=
# GNUBOK_CONNECT_URL=https://app.gnubok.se
# Outbound email (invoices, reminders). Pick one provider. Resend is what
# hosted runs; SMTP is for self-hosts that want mail on their own relay.
# EMAIL_PROVIDER=resend|smtp # optional; RESEND_API_KEY wins, else SMTP_HOST
# RESEND_API_KEY=
# RESEND_FROM_EMAIL=
# RESEND_INBOUND_DOMAIN= # invoice-inbox extension: per-company inbox addresses are {local-part}@{this domain}
# RESEND_INBOUND_WEBHOOK_SECRET= # invoice-inbox extension: verifies the Resend inbound webhook signature
# SMTP_HOST=
# SMTP_PORT=587
# SMTP_SECURE=false # true = implicit TLS on 465, false = STARTTLS required (set SMTP_REQUIRE_TLS=false only for a plaintext LAN relay)
# SMTP_USER=
# SMTP_PASS=
# SMTP_FROM_EMAIL=
# SMTP_REQUIRE_TLS=true # false only for a plaintext relay on a trusted LAN
# SMTP_TLS_REJECT_UNAUTHORIZED=true
# Bank connections (Enable Banking), own credentials. Set APP_ID and PRIVATE_KEY
# as a pair: any one of them (or a _PRODUCTION variant) switches the bank
# upstream out of connector mode. Leave all unset to route bank sync through
# GNUBOK_CONNECTOR_KEY. See docs/SELF-HOSTING.md, "Own credentials".
# ENABLE_BANKING_APP_ID=
# ENABLE_BANKING_PRIVATE_KEY= # base64-encoded PEM (bare base64 DER also works; raw PEM does not)
# ENABLE_BANKING_API_URL=https://api.enablebanking.com # default; api.tilisy.com = sandbox
# ENABLE_BANKING_PSU_TYPE=business
# Skatteverket API (VAT/AGI submission, skattekonto), own credentials. Setting
# either client id switches the upstream out of connector mode. Every base URL
# defaults to Skatteverket's TEST environment (test BankID only): set all five
# for production. SKATTEVERKET_ENABLED and the encryption key are needed in
# connector mode too. SKATTEVERKET_SYSTEM_* and SKATTEVERKET_OMBUD_ORG_NUMBER
# are hosted-only. See docs/SELF-HOSTING.md, "Own credentials".
# SKATTEVERKET_ENABLED=true
# SKATTEVERKET_OAUTH2_CLIENT_ID=
# SKATTEVERKET_OAUTH2_CLIENT_SECRET=
# SKATTEVERKET_APIGW_CLIENT_ID=
# SKATTEVERKET_APIGW_CLIENT_SECRET=
# SKATTEVERKET_TOKEN_ENCRYPTION_KEY= # openssl rand -base64 32; rotating it forces every user to reconnect
# SKATTEVERKET_ENV=production # test | production; defaults to test
# SKATTEVERKET_OAUTH_BASE_URL=https://peroauth2.skatteverket.se/oauth2/v1/per
# SKATTEVERKET_API_BASE_URL=https://api.skatteverket.se/momsdeklaration/v1
# SKATTEVERKET_AGD_INLAMNING_API_BASE_URL=https://api.skatteverket.se/arbetsgivardeklaration/inlamning/v1
# SKATTEVERKET_AGD_PERIOD_API_BASE_URL=https://api.skatteverket.se/arbetsgivardeklaration/hanteraredovisningsperiod/v1
# SKATTEVERKET_SKATTEKONTO_API_BASE_URL=https://api.skatteverket.se/beskattning/skattekonto/v2
# SKATTEVERKET_DISABLED=true # emergency kill switch
# Peppol e-invoicing via Qvalia (certified Access Point + SMP, partner model).
# The adapter registers itself when QVALIA_API_KEY, QVALIA_PARTNER_REG_NO and
# QVALIA_BASE_URL are all set; PEPPOL_TRANSPORT_PROVIDER=qvalia switches it on
# for users. Sandbox https://api-test.qvalia.com, production https://api.qvalia.com,
# separate keys per environment. QVALIA_ACCOUNT_REG_NO defaults to the partner
# number (consolidated setup). QVALIA_WEBHOOK_SECRET is the value Qvalia echoes
# in the QVALIA_WEBHOOK_HEADER on every delivery event (configure it with
# POST /partner/{p}/webhook/{id}/auth, type api_key). The key is sent bare in
# the Authorization header (what the sandbox accepts); QVALIA_AUTH_SCHEME=apikey
# switches to the "ApiKey <key>" form from the newest docs.
# PEPPOL_TRANSPORT_PROVIDER=qvalia
# Cap on companies that may publish a receiving identifier through the
# provider account (the Qvalia partner contract is priced per tenant, 10 to
# start). Unset = no cap.
# PEPPOL_RECEIVING_MAX_REGISTRATIONS=10
# QVALIA_API_KEY=
# QVALIA_PARTNER_REG_NO=
# QVALIA_ACCOUNT_REG_NO=
# QVALIA_BASE_URL=https://api-test.qvalia.com
# QVALIA_WEBHOOK_SECRET=
# QVALIA_WEBHOOK_HEADER=x-accounted-webhook-key
# QVALIA_AUTH_SCHEME=raw
# Accounting integrations
# FORTNOX_CLIENT_ID=
# FORTNOX_CLIENT_SECRET=
# FORTNOX_REDIRECT_URI=
# Self-hosted only: which scopes YOUR Fortnox app registration carries in the
# Fortnox Developer Portal. Unset means the hosted deployment's defaults
# (documents true, assets false), which describe the hosted app, not yours.
# Set to the registration's actual state: claiming an unapproved scope makes
# Fortnox reject authorize with invalid_scope before login, and denying an
# approved one just leaves that import feature off.
# FORTNOX_DOCUMENT_SCOPES_APPROVED=true # Arkivplats + Koppla filer (underlag import)
# FORTNOX_ASSET_SCOPES_APPROVED=false # Anlaggningsregister (asset register import)
# Björn Lundén app credentials (OAuth2 client credentials; per-company
# User-Key is entered by the user in the migration wizard)
# BJORN_LUNDEN_CLIENT_ID=
# BJORN_LUNDEN_CLIENT_SECRET=
# WhatsApp receipt intake (whatsapp-inbox extension, Meta Cloud API).
# ACCESS_TOKEN: system-user permanent token with whatsapp_business_messaging
# scope only. PHONE_NUMBER_ID: the Graph object id of the sending number.
# APP_SECRET verifies X-Hub-Signature-256 on the webhook; VERIFY_TOKEN is the
# GET-handshake shared secret you also enter in the Meta app dashboard.
# PHONE_HASH_KEY: random pepper for phone lookup hashes (openssl rand -hex 32).
# PHONE_ENCRYPTION_KEY: 32-byte hex AES-256-GCM key (openssl rand -hex 32).
# WHATSAPP_ACCESS_TOKEN=
# WHATSAPP_PHONE_NUMBER_ID=
# WHATSAPP_APP_SECRET=
# WHATSAPP_VERIFY_TOKEN=
# WHATSAPP_PHONE_HASH_KEY=
# WHATSAPP_PHONE_ENCRYPTION_KEY=
# Optional: the public number as E.164 digits (e.g. 46766867041) for the
# wa.me deep link in settings. Unset = resolved from the Graph API instead.
# WHATSAPP_PUBLIC_NUMBER=
# Bolagsverket: digital inlämning av årsredovisning (bolagsverket extension).
# BOLAGSVERKET_ENV is test | accept | prod (default test) and also caps which
# environment a company may select in settings (test < accept < prod).
# Certificate material is read from env ONLY (PEM or base64-wrapped PEM):
# never from extension settings or the database.
#
# SECRET CUSTODY (prod): never keep the real mTLS private key in a plaintext
# .env file. Inject these at runtime from a secrets manager (Vercel encrypted
# env vars, AWS Secrets Manager, Vault, Doppler, …), restrict read access to
# the deploy pipeline, and rotate the client certificate/key on the cadence
# agreed with Bolagsverket (and immediately on suspected exposure). Outbound
# hosts are pinned per environment in extensions/general/bolagsverket/lib/
# client.ts (HOSTS): the endpoint is not configurable via env.
# BOLAGSVERKET_ENV=
# BOLAGSVERKET_CLIENT_CERT=
# BOLAGSVERKET_CLIENT_KEY=
# BOLAGSVERKET_CA=
# Safety gate: enable only after agreement, certificate, test-bank fixtures,
# acceptance testing, and production runbook approval are complete.
# BOLAGSVERKET_FILING_ENABLED=false
# NEXT_PUBLIC_BOLAGSVERKET_FILING_ENABLED=false
# BOLAGSVERKET_ARELLE_VALIDATOR_URL=
# BOLAGSVERKET_ARELLE_VALIDATOR_TOKEN=
# ── Optional: product analytics + error tracking (PostHog) ─
# Hosted only. Self-hosted deployments never load PostHog: isAnalyticsEnabled()
# (lib/analytics/enabled.ts) short-circuits on NEXT_PUBLIC_SELF_HOSTED=true, and
# no __NEXT_PUBLIC_POSTHOG_*__ sentinel is baked into the Docker image, so an
# operator cannot accidentally ship their users' behaviour to our project.
#
# The token is the PUBLIC project token (phc_...). It is embedded in the client
# bundle by design and is not a secret. Leave unset to run with analytics off.
# Browser traffic goes through the same-origin /rl rewrite in next.config.ts;
# NEXT_PUBLIC_POSTHOG_HOST is only used by the server-side SDK.
# NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN=
# NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
#
# PostHog Support identity verification. A REAL SECRET (it also authenticates
# external API requests), so no NEXT_PUBLIC_ prefix: it must never reach the
# client bundle. Only the derived per-user HMAC crosses to the browser
# (lib/analytics/identity-hash.ts). Unset means support tickets are scoped to
# one browser session and users recover them by email link, which is the
# normal state for local dev, CI and self-hosted.
# POSTHOG_SECRET_API_KEY=
# ── Optional: error tracking / observability ──────────────
# The app routes every error-level log line, and anything flagged
# `alert: true`, to a provider-agnostic sink (lib/observability). When the
# PostHog token above is set, lib/init.ts registers the PostHog adapter
# (lib/analytics/posthog-observability.ts) as that sink; otherwise the sink
# stays a NO-OP, the PostHog client is never constructed and nothing is ever
# sent. (The SDK is still bundled in those builds, since the imports are
# static; it simply never initialises.) The variables below are for a
# DIFFERENT vendor adapter and still change nothing on their own.
#
# Names are generic placeholders. When a provider is picked, either keep these
# and read them in the adapter, or replace them with the vendor's own names.
# OBSERVABILITY_DSN= # server-side ingest endpoint / key
# NEXT_PUBLIC_OBSERVABILITY_DSN= # browser ingest endpoint / key, if used
# Any adapter reading these MUST forward only post-redaction payloads
# (lib/observability/redact.ts): see docs/security/logging-and-observability.md
# Optional overrides. Both have sensible defaults: the environment falls back
# to VERCEL_ENV then NODE_ENV, and the release falls back to
# NEXT_PUBLIC_BUILD_ID (the commit sha next.config.ts inlines at build time)
# then VERCEL_GIT_COMMIT_SHA. Set them only when tagging must differ.
# OBSERVABILITY_ENVIRONMENT=
# OBSERVABILITY_RELEASE=