Repository navigation
Expand file tree
/
Copy path.env.example
More file actions
432 lines (407 loc) · 24.5 KB
/
Copy path.env.example
File metadata and controls
432 lines (407 loc) · 24.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# LangX — environment template
#
# This repository is public (BSD-3). NO REAL VALUE EVER GOES IN A COMMITTED FILE.
# Copy to `.env` (git-ignored) for local dev; use the platform secret store in
# staging/produ
8000
ction. Anything prefixed EXPO_PUBLIC_ is compiled into the client
# bundle and is world-readable by design — never put a secret behind that prefix.
#
# **Two files, not one.** The API is told where to look (`--env-file=../../.env`),
# so everything below reaches it from this repository root. Expo is not: it reads
# `.env` from its own project root, `apps/mobile/`, and nowhere else. An
# `EXPO_PUBLIC_*` value left only in the root file is silently ignored — the app
# reads `undefined` and falls back, which is how a missing RevenueCat key looked
# exactly like a working one on 3 September 2026. Keep the `EXPO_PUBLIC_*` block
# in `apps/mobile/.env` as well:
#
# grep '^EXPO_PUBLIC_' .env > apps/mobile/.env
# ─── API: runtime ────────────────────────────────────────────────────────────
NODE_ENV=development
PORT=4000
HOST=0.0.0.0
LOG_LEVEL=debug
# How many proxies every request is certain to pass through, each appending to
# X-Forwarded-For. Unset: 1 under NODE_ENV=production (Fly's proxy), 0
# otherwise. A CDN the origin can be reached around does not count — one hop
# too many and a client chooses the address the rate limiter counts it under.
TRUST_PROXY_HOPS=
# ─── API: database ───────────────────────────────────────────────────────────
# Atlas SRV string, or `mongodb://127.0.0.1:27017` for a local mongod.
MONGODB_URI=mongodb://127.0.0.1:27017
MONGODB_DB=langx_dev
# ─── API: auth (Faz 1) ───────────────────────────────────────────────────────
# Required. Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET=
# Optional — defaults to http://<HOST>:<PORT> when unset.
BETTER_AUTH_URL=http://localhost:4000
# Comma-separated origins allowed to send credentialed requests.
# Shared with the Cloudflare transform rule that stamps every request coming
# through the edge. Without it the API cannot tell a real CF-IPCountry header
# from one somebody sent straight to the origin's IP, and it stops believing
# either — onboarding then asks for the country instead of reading it.
EDGE_SECRET=
# Required under NODE_ENV=production: the API refuses to start with it empty.
TRUSTED_ORIGINS=http://localhost:8081,http://localhost:19006
# Google Cloud Console → APIs & Services → Credentials → OAuth client ID.
# Authorized redirect URI: {BETTER_AUTH_URL}/api/auth/callback/google
# Create it as a **Web application** client, even for the phone: the app never
# talks to Google, it opens a browser at this API and the API does the
# exchange. Separate iOS/Android OAuth clients are not needed and do not work
# here. The browser returns to the app through the `langx://` scheme, which is
# why both schemes are in TRUSTED_ORIGINS.
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Apple Developer → Certificates, Identifiers & Profiles.
# APPLE_CLIENT_ID is the Services ID (not the app's bundle ID).
# Redirect URI in the Services ID config: {BETTER_AUTH_URL}/api/auth/callback/apple
# — Apple only allows HTTPS redirect URIs, so this cannot be tested against a
# plain http://localhost URL; it needs a real deploy or an HTTPS tunnel.
# APPLE_PRIVATE_KEY is the .p8 from a Sign in with Apple key, PEM content with
# real newlines replaced by literal \n (see auth/appleClientSecret.ts).
# The Services ID is still what Android and web use. On iOS the app shows
# Apple's own sheet instead, which returns a token issued to the *bundle*
# identifier — accepted because auth.ts declares appBundleIdentifier. Both
# routes land on the same account; no extra variable is involved.
APPLE_CLIENT_ID=
APPLE_TEAM_ID=
APPLE_KEY_ID=
APPLE_PRIVATE_KEY=
# Apple will not save the Services ID's return URL until it has fetched a file
# it generates from the *return URL's own host* — this API, not the web app:
# https://<api host>/.well-known/apple-developer-domain-association.txt
# Paste that file's contents here and the API serves them at that path. Public,
# not secret. Unset, the path 404s and the Services ID cannot be configured.
APPLE_DOMAIN_ASSOCIATION=
# Meta for Developers → create an app with the "Authenticate and request data
# from users with Facebook Login" use case. FACEBOOK_CLIENT_ID is the App ID,
# FACEBOOK_CLIENT_SECRET the App Secret (App settings → Basic).
# Valid OAuth Redirect URI: {BETTER_AUTH_URL}/api/auth/callback/facebook
# Like Google, every platform uses this one web flow — no Facebook SDK in the
# app. The `email` permission must be added to the use case, or the profile
# comes back without an address and the sign-in fails.
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
# Discord Developer Portal → Applications → OAuth2. DISCORD_CLIENT_ID and
# DISCORD_CLIENT_SECRET are the Client ID and Client Secret there.
# Redirect: {BETTER_AUTH_URL}/api/auth/callback/discord
# One web flow on every platform, like Google and Facebook. Scopes are
# `identify` and `email`; no bot is involved.
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
# ─── API: bot check ──────────────────────────────────────────────────────────
# Cloudflare Turnstile → the widget's secret key. Optional: unset, the captcha
# plugin is not registered and email sign-up, sign-in and password reset work
# without a token. Set, a token that arrives is checked; whether a *missing*
# one is refused is `flags.captchaRequired` in the app config, not this file —
# see docs/decisions.md → "Turnstile on the password forms". Cloudflare's test
# secret 1x0000000000000000000000000000000AA passes every token.
TURNSTILE_SECRET_KEY=
# ─── API: email (Faz 1) ──────────────────────────────────────────────────────
# Without a key, verification/reset emails are logged instead of sent — the
# app still boots and auth is fully testable locally. So are they under any
# NODE_ENV but `production`: a development process holding a real key must
# not mail the people in its database from the live sending domain.
RESEND_API_KEY=
# resend.dev needs no domain verification, so this works immediately. Point
# it at a verified sender of your own before launch — and at a mailbox a
# person reads rather than a no-reply@: replies are an engagement signal to
# the mailbox providers, and a no-reply sender throws away both that signal
# and whatever the person was trying to tell you.
EMAIL_FROM=LangX <onboarding@resend.dev>
# Where a bug report or feature request from the app lands. Nothing is stored
# server-side, so this mailbox is the whole record: it confirms the report and
# decides the reward.
# Self-hosting? Point it at your own address.
SUPPORT_EMAIL=hi@langx.io
# Which repository a bug report or feature request becomes an issue on.
GITHUB_ISSUE_REPO=langx/langx
# Opens that issue automatically, labelled triage. Fine-grained token with
# Issues: read and write on that one repository, nothing else. Optional: unset,
# the support email carries a prefilled "open this as an issue" link instead,
# and when it expires the email says opening failed.
GITHUB_ISSUE_TOKEN=
# Signs the unsubscribe link in every notification email. Its own secret, not
# BETTER_AUTH_SECRET, because rotating the auth secret must not break links
# that sit in inboxes for years — those links are the legal way out of the
# mail. Generate once and NEVER rotate: openssl rand -base64 32
# Unset, it falls back to BETTER_AUTH_SECRET, which works and inherits that
# problem.
EMAIL_UNSUBSCRIBE_SECRET=
# The Resend audience the langx.io newsletter form subscribes to. Took over
# from v1's SendGrid list when api.langx.io moved to this API.
RESEND_AUDIENCE_ID=
# Signing secret of the Resend webhook for bounces and complaints
# (POST /webhooks/resend). Unset, the route says it is not configured.
RESEND_WEBHOOK_SECRET=
# "Comment LANGX and I'll DM you the link" (POST /webhooks/instagram). The app
# secret verifies Meta's signature, the verify token is ours to invent and is
# only echoed during the subscription handshake, and the page token sends the
# replies. IG_ACCOUNT_ID is only a fallback: the account is normally asked of
# the token itself through /me, because the id the dashboard shows and the one
# the token answers with are not always the same.
# Unset, both routes say the feature is not configured and nothing else cares.
IG_APP_SECRET=
IG_VERIFY_TOKEN=
IG_PAGE_TOKEN=
IG_ACCOUNT_ID=
# Venus's day videos posted to our own accounts on the approved calendar's
# schedule (docs/social-autoposting.md). Nothing posts until the
# socialAutopost flag is on, and each platform is optional on its own: without
# its keys, its rows fail as "not connected" when due.
# Instagram posts with IG_PAGE_TOKEN above, once that token was granted
# instagram_business_content_publish.
# Facebook: the Page's id, and a Page access token with pages_manage_posts,
# pages_read_engagement and pages_show_list (Graph API Explorer → the Page →
# Get Page Access Token, from a long-lived user token so it never expires).
FB_PAGE_ID=
FB_PAGE_TOKEN=
# YouTube: an OAuth client (Google Cloud → APIs & Services → Credentials) of a
# project with the YouTube Data API v3 enabled, and the refresh token the
# channel owner granted it with the youtube.upload scope. Publish the consent
# screen first: in "Testing" the refresh token dies after seven days. Uploads
# stay private until Google audits the project.
YOUTUBE_CLIENT_ID=
YOUTUBE_CLIENT_SECRET=
YOUTUBE_REFRESH_TOKEN=
# TikTok: a TikTok for Developers app's client key and secret, and the refresh
# token the account granted it (video.upload; video.publish for direct posts).
# TikTok may return a new refresh token on each refresh; the newest is kept in
# the database and this one is only the seed.
TIKTOK_CLIENT_KEY=
TIKTOK_CLIENT_SECRET=
TIKTOK_REFRESH_TOKEN=
# inbox (default): each video lands as a draft in the TikTok inbox and is
# posted with one tap — the only public route before TikTok audits the app.
# direct: posts publicly by itself; switch to it after the audit.
TIKTOK_POST_MODE=inbox
# Reply-To on campaign mail, so "reply to this email" from a no-reply@ sender
# lands with a person. Unneeded when EMAIL_FROM is itself a mailbox somebody
# reads — langx.io sends from hi@langx.io and leaves this empty.
EMAIL_REPLY_TO=
# ─── API: storage (Faz 2 / 11) ───────────────────────────────────────────────
# S3-compatible; the published app runs on Backblaze B2 (same account as v1,
# bucket `langx-media`, region eu-central-003). R2 runs on the same code path.
# For B2 the region is the middle of the S3 endpoint host, and the public base
# URL is `https://f00N.backblazeb2.com/file/<bucket>` unless a CDN sits in
# front. STORAGE_PUBLIC_BASE_URL becomes part of every stored URL — pick it
# once, before any real upload or the migration ETLs. docs/self-host.md → Storage.
STORAGE_ENDPOINT=
STORAGE_REGION=auto
STORAGE_BUCKET=
STORAGE_ACCESS_KEY_ID=
STORAGE_SECRET_ACCESS_KEY=
STORAGE_PUBLIC_BASE_URL=
# Where ffmpeg lives, for the only thing this server converts: a voice note
# recorded in a browser is WebM/Opus and no iPhone can decode it. The Docker
# image installs ffmpeg, so this default is already right in production. Without
# it those notes are stored as recorded and the app says they will not play —
# every other kind of attachment is untouched either way.
FFMPEG_PATH=ffmpeg
# ─── API: the voice service (Echo "Read it aloud", chat "Show text") ─────────
# Where the service in apps/tts answers — Kokoro and Piper reading aloud,
# Whisper writing a voice note out. In production that is the app's private
# Flycast address, http://langx-tts.flycast — no port: 8080 is where the
# process listens inside its machine, while the Flycast address carries the
# ports [http_service] publishes, which are 80 and 443. Locally, it is the
# server run from apps/tts on its default port. Left unset, reading a member's
# own card aloud fails clearly, the app offers no transcripts, and nothing else
# changes — a pack's readings are made offline and never go through here.
# TTS_SECRET is the same value the service was started with; leave both empty
# to run without the check.
TTS_URL=
TTS_SECRET=
# ─── API: calls ──────────────────────────────────────────────────────────────
# The API carries no media. A call runs between the two devices, through a
# relay they both connect out to, and what the server needs is a way to hand
# each of them a short-lived credential for it.
#
# Cloudflare dashboard → Realtime → TURN → create a key: the key's id and its
# API token. Left unset in production, GET /app-config reports
# callService: false and the app draws no call button — there are no calls
# without a relay, on purpose, because the alternative is two strangers being
# told each other's IP address. Outside production an unset relay still lets
# two devices on one network call each other, which is all local work needs.
CLOUDFLARE_TURN_KEY_ID=
CLOUDFLARE_TURN_KEY_API_TOKEN=
# A relay of your own instead (coturn): a JSON array in the browser's
# iceServers shape, on one line, e.g.
# [{"urls":"turn:turn.example.com:3478","username":"u","credential":"p"}]
# Ignored when the Cloudflare pair above is set.
ICE_SERVERS_JSON=
# Ringing an iPhone whose app is not running takes a VoIP push, which goes to
# Apple directly — it is the one push Expo's relay does not carry. Apple
# Developer → Keys → an APNs auth key (.p8): its Key ID, and the file's content
# on one line with newlines escaped as
, same as APPLE_PRIVATE_KEY. Left
# unset, an iPhone is rung by an ordinary notification and has to be opened to
# answer; everything else about calling is unchanged. APNS_TEAM_ID and
# APNS_BUNDLE_ID default to this app's own and only matter to a fork shipping
# under a different identity.
APNS_KEY_ID=
APNS_PRIVATE_KEY=
APNS_TEAM_ID=
APNS_BUNDLE_ID=
# ─── API: translation (Faz 6) ────────────────────────────────────────────────
# Google Cloud Console → a project with the Cloud Translation API enabled →
# IAM & Admin → Service Accounts → create one with the "Cloud Translation
# API User" role → Keys → Add key (JSON) → paste the whole file's content
# below on one line (escape newlines in private_key as \n, same as
# APPLE_PRIVATE_KEY). Left unset, /translate fails clearly; nothing else
# depends on it.
GOOGLE_TRANSLATE_PROJECT_ID=
GOOGLE_TRANSLATE_SERVICE_ACCOUNT_JSON=
# ─── API: billing (Faz 7) ────────────────────────────────────────────────────
# RevenueCat dashboard → API Keys → Secret API key. Used by POST /billing/refresh
# to reconcile straight from RevenueCat when a webhook is late or lost.
REVENUECAT_SECRET_API_KEY=
# RevenueCat dashboard → your webhook's "Authorization header value" — a
# shared secret you pick yourself, not a value RevenueCat generates. Left
# unset, POST /webhooks/revenuecat refuses every request (fails closed).
REVENUECAT_WEBHOOK_AUTH_HEADER=
# `true` swaps RevenueCat for an in-process fake and exposes
# POST /billing/test-event, so a purchase can be driven end to end with no
# store product and no network — see docs/billing-testing.md. Takes precedence
# over REVENUECAT_SECRET_API_KEY above, and the API refuses to boot with it set
# under NODE_ENV=production. Leave blank unless you are testing billing.
REVENUECAT_FAKE_STORE=
# Not read by this backend at all — RevenueCat Web's Stripe connection is a
# one-time dashboard configuration on RevenueCat's side, not a runtime API
# call from here. Kept as a placeholder for that account's own bookkeeping.
STRIPE_SECRET_KEY=
# ─── API: observability ──────────────────────────────────────────────────────
SENTRY_DSN=
# ─── API: migration (Faz 2 / 11) ─────────────────────────────────────────────
APPWRITE_ENDPOINT=
APPWRITE_PROJECT_ID=
APPWRITE_API_KEY=
# Salt for hashing legacy emails into handleReservations.legacyEmailHash.
LEGACY_EMAIL_HASH_SALT=
# Only needed if the Expo project has enhanced push security switched on, in
# which case unauthenticated sends are rejected. Expo dashboard → Access
# tokens. Normally left blank.
EXPO_ACCESS_TOKEN=
# ─── Mobile/web client (public — shipped in the bundle) ──────────────────────
EXPO_PUBLIC_API_URL=http://localhost:4000
# Cloudflare Turnstile → the widget's site key. Public by design (it sits in
# every page that shows the widget); its secret belongs to TURNSTILE_SECRET_KEY
# and never under this prefix. Unset, no widget is drawn and no token is sent,
# which a server without the secret, or with `flags.captchaRequired` off,
# accepts. Cloudflare's test key 1x00000000000000000000AA always passes and
# works on localhost; the production key only works on langx.io hostnames.
EXPO_PUBLIC_TURNSTILE_SITE_KEY=
# Path to the Firebase Android config for FCM. Gitignored (this repo is
# public), and app.config.ts leaves googleServicesFile unset without it — the
# build succeeds and Android simply receives no remote notifications.
GOOGLE_SERVICES_JSON=
# Google Cloud → Maps SDK for Android key, for the map in a shared location's
# chat bubble. Not EXPO_PUBLIC_: app.config.ts writes it into the Android
# manifest at build time, so it belongs in apps/mobile/.env for a local build
# and in the EAS `production` environment for a real one. Restrict it to the
# app's package name and signing certificates in the Cloud console — that
# restriction, not secrecy, is what protects it (it ships inside every APK).
# Unset, Android shows the location card without a map; iOS uses Apple Maps
# and needs no key.
GOOGLE_MAPS_ANDROID_API_KEY=
# RevenueCat dashboard → API keys → SDK API keys. Public by design: these
# identify the app and are compiled into the bundle. The secret key belongs to
# REVENUECAT_SECRET_API_KEY above and must never appear under this prefix.
#
# The per-platform pair is what a released build uses, and each needs a real
# App Store / Play configuration in RevenueCat to exist at all. Until then the
# project has only a Test Store app, whose single key covers both platforms —
# set that one and leave the pair blank. With none of them set, the paywall
# renders and says purchasing is unavailable; nothing else in the app changes.
EXPO_PUBLIC_REVENUECAT_IOS_KEY=
EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=
# The browser buys from RevenueCat Web Billing, which is a third store and not
# a third platform: its own RevenueCat app, its own Stripe connection, its own
# products and offering, and a key starting `rcb_`. Unset, the web paywall says
# purchasing is unavailable and nothing else changes. The Test Store key below
# also works here in a development build, which is how the web checkout can be
# exercised before Stripe is connected.
EXPO_PUBLIC_REVENUECAT_WEB_KEY=
EXPO_PUBLIC_REVENUECAT_TEST_STORE_KEY=
# `1` is the client half of REVENUECAT_FAKE_STORE: the paywall lists every
# package at obviously fake prices and buying one calls the API's fake store.
# Ignored unless the bundle is a development one, so it cannot follow a web
# export into production. Both halves have to be set for a purchase to land.
EXPO_PUBLIC_REVENUECAT_FAKE_STORE=
# `1` draws the on-screen debug panels (today: the profile's daily quota
# counters). Ignored unless the bundle is a development one, for the same
# reason as the flag above.
EXPO_PUBLIC_DEBUG_PANEL=
EXPO_PUBLIC_SENTRY_DSN=
# PostHog → Project settings → Project API key, and the ingestion host of the
# region the project lives in (EU by default; see docs/decisions.md). The key
# is a *write* key — anyone holding the bundle has it, and all it allows is
# sending events — but it still stays out of the repo: a key committed to a
# public repository is an invitation to fill the project with junk. Unset, the
# SDK is never loaded, Settings shows no analytics row, and nothing leaves the
# device. What is sent, and the store-form answers it implies, are in
# docs/analytics.md and docs/store/privacy-data-safety.md.
EXPO_PUBLIC_POSTHOG_KEY=
EXPO_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
# `pnpm insight` reads the funnel back out of that project and writes it to a
# private temporary file — the counterpart of insight.langx.io. The key is a
# *personal* API key (PostHog → Settings → Personal API keys), and PostHog
# scopes them: this one needs **read** access to the project and nothing more,
# so it stays on the machine that runs the command and never anywhere else.
# The project id is the number in that project's own URL.
#
# The API wants the same three names for a different job — deleting a person
# when their account is purged — and it must NOT be given this key. That one
# needs `person:write`, which is the difference between a credential that can
# read your analytics and one that can erase them, and it lives on the server
# rather than a laptop. Two keys, two scopes, two places. Unset on the API,
# the purge still records what it owes and the drain does nothing; see
# apps/api/src/modules/account/analyticsDeletions.ts.
#
# A key PostHog does not accept answers 403, not 401. A placeholder left in
# this file is the usual reason, and the script says so.
#
# The region is `eu` or `us` rather than a host, and that is deliberate: the
# API lives in exactly those two places, the host it resolves to is a literal
# in the script, and a settable one would only ever be a way to send that key
# somewhere it was not meant to go. It is also not `eu.i.posthog.com` — that
# is where the app sends events and it does not serve this API.
POSTHOG_PERSONAL_API_KEY=
POSTHOG_PROJECT_ID=
POSTHOG_REGION=eu
# The funnel on the operator panel: install -> onboarding -> first message ->
# paywall, over the last 30 days and over all time. A PostHog personal API key
# scoped `query:read` on the same project.
#
# A second variable rather than another scope on POSTHOG_PERSONAL_API_KEY
# above. That one is `person:write` so the account purge can delete people, and
# a single key that can both read the whole project and delete from it is the
# credential this split exists to avoid. It can be the same key `pnpm insight`
# reads from this file — that one is already read-only.
#
# Unset is fine: the panel says the funnel is not configured and nothing else
# on it changes.
POSTHOG_QUERY_API_KEY=
# The assistant behind @langx — the account that greets every new user, carries
# announcements and answers when somebody writes to it.
#
# Optional, and unset is a supported way to run LangX. Without a key the two
# official accounts are still created at boot, the welcome message and
# announcements still go out (those are ours, not the model's), and a message
# to @langx is answered with a line saying it cannot answer right now and
# pointing at SUPPORT_EMAIL. @copilot's canned reply never needed a key at all.
#
# Two ceilings, both in packages/shared/src/officialAccounts.ts:
# `repliesPerDay` bounds one conversation and `globalRepliesPerDay` bounds the
# bill, because the number of conversations is not bounded by anything. Past
# either one the assistant says so and points at SUPPORT_EMAIL.
#
# The default model is Sonnet 5. Haiku 4.5 is cheaper and was tried; it read
# Turkish questions wrongly, which is the wrong thing to save money on in an app
# that answers in eight languages.
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-sonnet-5
# Copilot — Check, Reply ideas and Explain in a chat — uses the same key. The
# key does not turn it on: Copilot answers only once the operator sets
# `flags.copilotEnabled` (scripts/maintenance.ts flag copilotEnabled true), and
# only for people who agreed to the consent sheet. Unset, the model is
# ANTHROPIC_MODEL's. The allowances in PLAN_LIMITS.copilotPer24h are sized for
# Sonnet 5's price; re-do that sum before choosing a different model.
COPILOT_MODEL=