Files
accounted/.env.example
T
Mattsson 3829b6add3 fix(ai): complete plain-key self-hosting path (#1584)
* feat(ai): resolve the Claude backend from the environment

Tier 1 of #1406: a self-hosted deployment can now run every AI feature on a
plain ANTHROPIC_API_KEY, with no AWS account. Hosted behaviour is unchanged.

lib/ai/provider.ts resolves the backend once, from the environment:

  AI_PROVIDER              explicit override, bedrock|anthropic
  AWS static key pair      Bedrock
  ANTHROPIC_API_KEY        the direct Anthropic API
  nothing set              Bedrock, so the AWS credential provider chain
                           (instance profile, IRSA) still resolves

Bedrock deliberately wins when both credential sets are present. EU residency
in eu-north-1 is a BFL/GDPR posture rather than a default, so adding an
Anthropic key for an experiment must not silently move production inference
out of the region. AI_PROVIDER is the way to say you meant it.

Model ids are written bare in code and prefixed to eu.anthropic.* only for
Bedrock, which needs the cross-region inference profile for on-demand
throughput. An operator override that already carries a prefix passes through
untouched, so BEDROCK_MODEL_ID and friends keep working as written.

Converted call sites: the agent composer, invoice-inbox extraction, the
document-extraction model label, and both receipt-hunt clients. The last two
are not named in the issue, which predates receipt-hunt landing in main.

@anthropic-ai/sdk is declared at 0.95.0, the version @anthropic-ai/bedrock-sdk
0.29.1 already pulled in transitively, so the lockfile dedupes to one copy
with no new download.

scripts/smoke-bedrock.ts becomes scripts/smoke-ai.ts and grows two steps.
Unit tests can only prove which provider and model id get resolved; they
cannot prove the resulting request is one the backend accepts. The script now
sends real traffic over all three shapes the app uses: a plain create, a
streamed turn carrying adaptive thinking, an effort level, an hour-long cache
breakpoint and a tool, and document extraction end to end when given a file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Bjorn Bergenheim <29535152+bjornbergenheim@users.noreply.github.com>

* docs(self-hosting): document the AI smoke test

The script added alongside the provider split is what closes the #1406
acceptance criterion ("document extraction and the assistant both work"), so
a self-hoster needs to know it exists. Covers both invocations and states
that it exits non-zero, which is what makes it usable as a post-deploy check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Bjorn Bergenheim <29535152+bjornbergenheim@users.noreply.github.com>

* test(ai): split the smoke test's thinking probe from its tool probe

The combined probe could not falsify what it claimed to. It asked a question
that needs a tool call, so the tool was used and adaptive thinking correctly
declined to reason about it: the zero thinking-block count that came back was
uninformative rather than a signal.

2a keeps the tool and drops thinking. 2b asks a question with several
dependent steps (reverse charge, then a partial deduction, then the affected
boxes) so that a model honouring the parameter must reason, and reports the
thinking text length as well as the block count, since display:"summarized"
can yield blocks with empty text.

The cached system prompt is also padded past the 1024-token minimum cacheable
prefix. Below that the API caches nothing and reports no error, so the old
probe's cache counters read zero whether or not caching worked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Bjorn Bergenheim <29535152+bjornbergenheim@users.noreply.github.com>

* fix(document-extraction): stop requiring AWS_REGION in the manifest

The extension now needs one of two credential sets, AWS static keys or
ANTHROPIC_API_KEY, and the manifest schema cannot express "one of". Since
requiredEnvVars only drives a build-time warning and never gates anything,
listing AWS_REGION told every self-hoster running the direct API to set a
variable that has no effect for them.

The description was also still promising Sonnet 4.6 via Bedrock specifically,
which is no longer what the extension does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Bjorn Bergenheim <29535152+bjornbergenheim@users.noreply.github.com>

* fix(ai): read documentKind defensively in the smoke test

The field arrived with the receipt-aware extraction work, so referencing it
directly stops the script compiling against any checkout from before that
landed. tsconfig includes **/*.ts and next.config does not disable type
checking, so on such a checkout this failed the production build rather than
just the script: caught while preparing a test branch for a self-hosted
instance that had not synced yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Bjorn Bergenheim <29535152+bjornbergenheim@users.noreply.github.com>

* fix(deps): restore the nested @swc/helpers entry in the lockfile

Declaring @anthropic-ai/sdk with `npm install --package-lock-only` also pruned
node_modules/next-intl/node_modules/@swc/helpers@0.5.23, an optional peer entry
the local npm 11 considers redundant and the image's npm 10.9.8 does not. The
result passed every local check and failed `npm ci` inside the Docker build,
which is the only place the lockfile is actually enforced.

The lockfile is now the previous one plus the single root dependency line,
verified with `npm ci --dry-run`. @anthropic-ai/sdk needed nothing else: it was
already in the tree as a transitive dependency of @anthropic-ai/bedrock-sdk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Bjorn Bergenheim <29535152+bjornbergenheim@users.noreply.github.com>

* Update DECISIONS.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update Docker documentation for AI provider credentials

Clarify the role of credentials in AI provider selection and document extraction requirements.

* Update SELF-HOSTING.md with smoke-ai script details

Clarify usage of smoke-ai script for credential checks and document extraction.

* Improve error handling and logging in smoke-ai script

* fix(ai): complete plain-key self-hosting path

Signed-off-by: Emil <emilmattsson14@gmail.com>

---------

Signed-off-by: Bjorn Bergenheim <29535152+bjornbergenheim@users.noreply.github.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>
Co-authored-by: Bjorn Bergenheim <29535152+bjornbergenheim@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-08-13 15:45:24 +02:00

174 lines
9.1 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
# 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). Two ways to provide a key;
# set one of them. AI_PROVIDER (bedrock|anthropic) forces the choice if both
# are present, which otherwise resolves to Bedrock.
#
# 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
# BEDROCK_MODEL_ID=
#
# 2. Claude via the direct Anthropic API. No AWS account needed, so this is
# usually the self-hosted option. Note that it has no EU-residency
# guarantee: use Bedrock if you need one.
# ANTHROPIC_API_KEY=
# AI_PROVIDER=
# Bank connections (Enable Banking)
# ENABLE_BANKING_APP_ID=
# ENABLE_BANKING_PRIVATE_KEY=
# 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=