Files
accounted/.env.example
T
Jakob Wennberg 9ef7de861f feat(peppol): poll outbound delivery status + cap receiving registrations (#1793)
* feat(peppol): poll outbound delivery status from the access point

Qvalia's webhook API answers 404 on its production host (the sandbox answers
204), so without this the prod lifecycle would stop at submission_accepted.
The transport gains pollDeliveryStatus(); the Qvalia adapter reads
/invoices/outgoing/status and maps the message-log status through the same
tolerant mapping as a document_delivery webhook, with the same dedupe key, so
a later webhook for the same transition is a harmless duplicate. A cron four
times an hour walks the open deliveries of the last 45 days, records the
answer through the append-only lifecycle RPC and fetches evidence once a
delivery reaches transport or a terminal state. Kept as the safety net for a
missed webhook once Qvalia ships them to prod.

Refs #546

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ

* feat(peppol): cap receiving registrations at the contracted tenant count

The Qvalia partner contract is priced per tenant (10 to start), so the
registration refuses the next company with PEPPOL_REGISTRATION_CAP_REACHED
once PEPPOL_RECEIVING_MAX_REGISTRATIONS live registrations exist, instead of
silently exceeding the contract. A company that already holds a live row is
never counted twice; unset means no cap (own provider account).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 17:06:57 +02:00

221 lines
12 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
# ── 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=
# Bank connections (Enable Banking)
# ENABLE_BANKING_APP_ID=
# ENABLE_BANKING_PRIVATE_KEY=
# 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=
# 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=