-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy path.env.example
More file actions
333 lines (295 loc) · 18.2 KB
/
Copy path.env.example
File metadata and controls
333 lines (295 loc) · 18.2 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
# ---------------------------------------------------------------------------
# White-label branding (see src/lib/site-config.ts)
# NEXT_PUBLIC_* vars are inlined at build time and safe to expose to the client.
# AUDIENCE is server-only (used in the chat system prompt).
# All have sensible defaults — override only what you want to rebrand.
# ---------------------------------------------------------------------------
# Display name shown in the header brand lockup and page titles.
NEXT_PUBLIC_SITE_NAME="MakerLAB Tools"
# Public origin link previews (og:image, og:url) resolve against. Optional:
# on Vercel the production domain (VERCEL_PROJECT_PRODUCTION_URL) is used,
# elsewhere https://makerlab-ai.vercel.app. Set it for a custom domain.
# NEXT_PUBLIC_SITE_URL=https://tools.example.edu
# Institution / organization name (used in metadata + chat prompt).
NEXT_PUBLIC_INSTITUTION="Cornell Tech"
# Tagline: under the site name in the header, and the site description.
NEXT_PUBLIC_TAGLINE="Your digital guide to making at Cornell Tech"
# Name the AI assistant refers to itself by.
NEXT_PUBLIC_CHAT_ASSISTANT_NAME="MakerLAB Assistant"
# Audience description woven into the chat system prompt (server-only).
AUDIENCE="students who may be beginners"
# Logo path under /public.
NEXT_PUBLIC_LOGO="/makerlab-logo-transparent.png"
# The wordmark alone ("MakerLAB"), under /public: the site header draws it as a
# mask in the theme's text colour, above the site name and tagline.
NEXT_PUBLIC_WORDMARK="/makerlab-wordmark.png"
# The lab's opening hours, one line of text: the header's status strip and the
# /kiosk lab status screen. Free text until hours are structured (kiosk spec
# phase 2); empty or unset means the default below.
NEXT_PUBLIC_LAB_HOURS="LAB OPEN 8AM-8PM"
# Brand colors — injected as the --primary / --primary-dark CSS variables
# at the root layout, so changing these re-themes the app without a CSS edit.
NEXT_PUBLIC_COLOR_PRIMARY="#ff6b35"
NEXT_PUBLIC_COLOR_PRIMARY_DARK="#cc4f1f"
# ── Notion data layer — IMPORT ONLY ──────────────────────────────────
# Postgres is the source of truth. Nothing the site serves reads Notion, and
# as of Phase 3 nothing the site writes goes to Notion either, except intake's
# `create_tool` (which uses NOTION_DB_TOOLS/CATEGORIES/LOCATIONS/UNITS/
# RESOURCES until Phase 6). Everything below is needed by
# `npm run import:notion`; MAINTENANCE_LOGS, FLAGS and PROJECTS are needed by
# NOTHING ELSE and can be removed from the deployment once the import has run.
#
# Internal integration token from https://www.notion.so/my-integrations
NOTION_API_KEY=ntn_YourTokenHere
# Notion database IDs (required by the import — share each with the integration)
NOTION_DB_TOOLS=YourDatabaseIdHere
NOTION_DB_CATEGORIES=YourDatabaseIdHere
NOTION_DB_LOCATIONS=YourDatabaseIdHere
NOTION_DB_UNITS=YourDatabaseIdHere
NOTION_DB_RESOURCES=YourDatabaseIdHere
# Import only — tickets are written to Postgres `maintenance_logs` now.
NOTION_DB_MAINTENANCE_LOGS=YourDatabaseIdHere
# Import only — corrections are written to Postgres `feedback` now.
NOTION_DB_FLAGS=YourDatabaseIdHere
# Optional, and import only: the Student Projects gallery's source database.
# Submissions are written to Postgres `projects` (unpublished until staff
# publish them), so the gallery works with this unset.
NOTION_DB_PROJECTS=
# ── AI: the Vercel AI Gateway (gateway spec 2026-09-23) ───────────────
# THE ONLY MODEL PATH. There is no direct-provider fallback and no
# ANTHROPIC_API_KEY any more — src/lib/ai/models.ts reads neither. Every model
# call (chat, research, image ranking, the intake identify step, image
# cleaning) goes through the Gateway, so model spend lands on one invoice with
# hosting and gets a platform-enforced ceiling. Create the Gateway in the
# Vercel dashboard under AI Gateway, and set a monthly spend limit at the same
# time (docs/deploy.md) — an uncapped key behind a public chat endpoint is the
# largest financial risk in this app.
#
# Local development: set this to a Gateway API key from the dashboard.
AI_GATEWAY_API_KEY=
# Production: set NO key at all. Vercel injects a short-lived OIDC token
# (VERCEL_OIDC_TOKEN) into every deployment automatically, and the Gateway
# provider falls back to it when AI_GATEWAY_API_KEY is unset — no secret to
# rotate, no key to leak. To exercise that same OIDC path locally (rather than
# a long-lived key), pull one with the Vercel CLI instead of setting a key
# here:
# vercel env pull
# This writes VERCEL_OIDC_TOKEN to .env.local, valid roughly 12 hours; run it
# again when it expires. Leave AI_GATEWAY_API_KEY blank to use it.
# Per-job model overrides — each names one Vercel AI Gateway model id, the
# shape "provider/model" in lower case (e.g. "openai/gpt-6-luna",
# "anthropic/claude-sonnet-5"); a malformed value is refused with a
# ModelConfigError naming the variable, never falls back silently. Blank (the
# default here) means "use the job's built-in default" — see MODEL_JOBS in
# src/lib/ai/models.ts, the one place a job becomes a model:
# chat → MODEL_CHAT (default openai/gpt-6-luna — passed
# the eval gate after prompt tuning; see evals/README.md)
# research search → MODEL_RESEARCH_SEARCH (default openai/gpt-6-luna)
# research read → MODEL_RESEARCH_READ (default openai/gpt-6-luna)
# image ranking → MODEL_IMAGE_RANK (default openai/gpt-6-luna)
# manual search → MODEL_EMBED (default openai/text-embedding-3-small,
# an *embedding* model, asked for 512 dimensions — the
# manual_chunks.embedding column is halfvec(512). Changing it
# makes every manual's passages stale; `npm run
# manuals:index` re-embeds them, which costs cents.
# voyage/voyage-4-lite also fits: see the manual text spec's
# phase-2 amendment for the comparison.)
# manual OCR → MODEL_OCR (default openai/gpt-6-luna, flex — reads
# scanned manual pages; only `npm run manuals:index` runs it)
# manual rerank → MODEL_RERANK (default cohere/rerank-v4-fast — a
# *reranking* model ordering search_manual's candidates;
# "off" turns reranking off, search keeps the fused order)
# starter grading → MODEL_STARTER_GRADE (default openai/gpt-6-luna, flex — judges
# the pre-run starter-chip answers; only `npm run
# starters:refresh` runs it)
# (There is no image-cleaning model: background removal is a deterministic
# cutout in code since 2026-09-23. MODEL_IMAGE_CLEAN is read by nothing.)
MODEL_CHAT=
MODEL_RESEARCH_SEARCH=
MODEL_RESEARCH_READ=
MODEL_IMAGE_RANK=
MODEL_EMBED=
MODEL_OCR=
MODEL_RERANK=
MODEL_STARTER_GRADE=
# Chat call options (src/lib/ai/models.ts chatProviderOptions; performance plan).
# MODEL_CHAT_REASONING — the chat model's reasoning effort: blank means "low";
# "default" sends no hint (the provider's own); or none, minimal, medium, high.
# MODEL_CHAT_CACHE_KEY — the prompt-cache key chat calls send: blank means
# "makerlab-chat-v1"; "off" sends none. Both reach an OpenAI-family model and
# are ignored by others. Run `npm run eval` after changing either.
MODEL_CHAT_REASONING=
MODEL_CHAT_CACHE_KEY=
# Per-job Gateway service tier — "default" (no hint), "flex" or "priority",
# any case; anything else is a ModelConfigError naming the variable. Blank
# means the job's own setting in MODEL_JOBS: flex for the background jobs
# (research search, research read — also the starter-question backfill — and
# image ranking), default for chat. A best-effort hint, applied whichever model
# is set; a provider without tiers ignores it. MODEL_EMBED_TIER is validated
# like the others but has no effect today: embedding calls send no tier hint.
MODEL_CHAT_TIER=
MODEL_RESEARCH_SEARCH_TIER=
MODEL_RESEARCH_READ_TIER=
MODEL_IMAGE_RANK_TIER=
MODEL_EMBED_TIER=
MODEL_OCR_TIER=
MODEL_STARTER_GRADE_TIER=
# Test/E2E only — overrides the Gateway's base URL (the full path, /v3/ai
# included; default https://ai-gateway.vercel.sh/v3/ai). Production never sets
# this; it exists so a test or the E2E stub (e2e/stubs/gateway-stub.ts) can
# point the provider at a local server instead of the real Gateway.
AI_GATEWAY_BASE_URL=
# E2E only — one exact origin (e.g. http://localhost:3101) the SSRF guard
# (src/lib/web/address-guard.ts) exempts from its loopback/private-address
# refusal, so the research read step and the chat's read_page tool can fetch
# pages the local Playwright stub serves. Ignored whenever VERCEL is set, so
# it has no effect in any real deployment. Leave unset outside E2E.
READ_PAGE_TEST_ORIGIN=
# Overrides the model `npm run eval` uses, independent of MODEL_CHAT — a
# Gateway id, same "provider/model" shape as the MODEL_* vars above (e.g.
# openai/gpt-6-luna, openai/gpt-6-sol, anthropic/claude-sonnet-5). See
# evals/README.md for the §10 eval gate this runs before switching MODEL_CHAT.
EVAL_MODEL=
# ── Admin ────────────────────────────────────────────────────────────
# Shared secret guarding POST /api/admin/revalidate. Also accepted by
# GET /api/cron/daily, so a person can trigger the nightly job by hand.
ADMIN_REVALIDATE_SECRET=YourSecretHere
# ── Files and the nightly job ────────────────────────────────────────
# Set in Vercel, not locally.
#
# Sent by Vercel Cron as `Authorization: Bearer $CRON_SECRET`, which is how
# GET /api/cron/daily knows the nightly call is genuine. Unset, that route
# answers 503 — deliberately, so a missing backup is never silent.
CRON_SECRET=YourCronSecretHere
# Optional. A heartbeat monitor's ping URL (Healthchecks.io or Better Stack,
# https only). Each nightly run pings it — `<url>` on success, `<url>/fail`
# otherwise — and the monitor emails when a ping fails or never arrives.
# Treat it like a secret. See docs/operations.md.
# CRON_HEARTBEAT_URL=https://hc-ping.com/your-uuid
# Injected automatically when a Blob store is linked to the Vercel project.
# It gates TWO things now:
# 1. Every photo upload (POST /api/uploads). Unset, the route answers 503
# `blob_not_configured` and the chat and project form say photo uploads
# are unavailable — they never hand back an id for a file nobody stored.
# 2. The nightly Postgres export written by GET /api/cron/daily.
# Uploads are stored public or private per kind; maintenance photos and the
# backup file contain student names and emails and are always PRIVATE.
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_YourTokenHere
# A Blob store is either all-public or all-private now, so a deployment links
# TWO: the one above (default prefix) is the PUBLIC store, and a PRIVATE store
# connected with the custom prefix `BLOB_PRIVATE` ("Add a read-write token"
# ticked) holds the backup and every private upload. The app routes each file
# by its access (`blobCredentials()` in src/lib/blob-mode.ts). Leave both unset
# for one store holding both kinds — local dev, or an older store.
# BLOB_PRIVATE_READ_WRITE_TOKEN=vercel_blob_rw_YourPrivateTokenHere
# BLOB_PRIVATE_STORE_ID=store_YourPrivateStoreId
# The lab's timezone, used for the date on a maintenance ticket. Defaults to
# America/New_York. A Vercel function runs in UTC, so without this a report
# filed at 9pm in New York would be dated tomorrow. A value Intl does not
# recognise falls back to UTC with a warning rather than losing the report.
LAB_TIMEZONE=America/New_York
# Usage insight (docs/specs/2026-09-27-usage-insight-design.md): anonymous
# counts of what the lab asks about, shown on /admin/insights. On unless set
# to `off`, which records nothing at all (the page keeps what it has).
# USAGE_INSIGHT=off
# ── Database (Neon Postgres) ─────────────────────────────────────────
# Injected automatically when Neon is installed from the Vercel Marketplace.
# With it unset the app, the tests and `npm run import:notion -- --dry-run`
# use an in-process Postgres (PGlite) with sample data, and `npm run
# db:migrate` does nothing. Set it to import for real and to run migrations.
DATABASE_URL=
# Local only: a persistent PGlite database in this directory (relative to the repo root),
# used when DATABASE_URL is unset — to import the real Notion inventory and
# review it in `npm run dev` before importing into Neon. Migrated on open,
# never demo-seeded, no demo banner; /api/health says `"database": "local"`.
# Single-process: stop the dev server while `npm run import:notion` writes to
# it. Refused on Vercel and in production builds (unset it before `next build`).
# PGLITE_DATA_DIR=.pglite-data
# ── Sign-in (Google Workspace) ───────────────────────────────────────
# Optional. With these unset the app runs exactly as before: everyone is
# anonymous, the catalog and the assistant still work, and /api/auth returns
# 503. AUTH_SECRET alone gives you sessions; add the two GOOGLE_* variables to
# make signing in possible.
#
# Signing key for the session cookie. Generate with:
# openssl rand -base64 32
# ROTATING THIS SIGNS EVERYONE OUT: every existing cookie stops verifying.
# (Individual revocation no longer needs it — sessions are rows since Phase 4,
# so a ban takes effect on the person's next request.) Also salts the hashed
# IPs the rate limiter stores, so rotating resets anonymous allowances too.
#
# This alone is enough for sessions. Without the two GOOGLE_* variables below
# there is simply no way to start one, and the header says sign-in is not set
# up here — which is how the E2E suite and a pre-OAuth deployment both run.
AUTH_SECRET=
# Google OAuth client (Web application) from console.cloud.google.com.
# Authorized redirect URI must be exactly:
# https://<your-domain>/api/auth/callback/google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Absolute origin the OAuth redirect returns to. Optional on Vercel (derived
# from VERCEL_PROJECT_PRODUCTION_URL / VERCEL_URL); required for a custom
# domain and for local development.
AUTH_BASE_URL=http://localhost:3000
# Email domain sign-in is restricted to. Enforced server-side in the callback;
# also sent to Google as the `hd` hint, which only narrows the account picker.
AUTH_ALLOWED_EMAIL_DOMAIN=cornell.edu
# Addresses allowed in BY NAME, whatever domain they are on — comma-separated.
# Named exceptions, never a second open domain: for a maintainer or a director
# whose institutional account is temporary. Setting any value here drops the
# Google `hd` hint, because Better Auth verifies that claim and a personal
# account carries none. Empty (the Cornell Tech deployment's setting) leaves
# the domain rule exactly as it was.
AUTH_ALLOWED_EMAILS=
# The super-admin FLOOR — comma-separated addresses. Not a roster: everyone
# else's role is the `user.role` column, changed on /admin/users.
#
# An address listed here is created as `super_admin` on first sign-in and
# resolves as `super_admin` whatever its row says, and cannot be demoted or
# banned. Two reasons, both structural:
# 1. Bootstrap — no user row exists until somebody signs in, so there is no
# admin to promote the first one. This is how the first one comes to be.
# 2. Lock-out — a super admin who demotes themselves would otherwise leave
# nobody able to undo it and no UI to fix it with.
# Use a permanent address. The Cornell Tech deployment uses ies22@cornell.edu.
#
# AUTH_STAFF_EMAILS and AUTH_ADMIN_EMAILS were removed in Phase 4. Nothing
# reads them; delete them from the deployment's environment, because a list
# that grants nothing is a misleading roster.
AUTH_SUPER_ADMIN_EMAILS=
# ── Development-only sign-in (LOCAL `npm run dev` ONLY) ──────────────
# !!! NEVER set these on Vercel or any deployment. A Vercel build that sets
# !!! DEV_AUTO_SIGN_IN fails on purpose (next.config.ts).
# With DEV_AUTO_SIGN_IN=1, `next dev` serves GET /api/dev/sign-in, which signs
# you in with a real database session and no Google round trip:
# http://localhost:3000/api/dev/sign-in?as=student@cornell.edu&next=/admin
# It creates the user if missing, with the role a first Google sign-in would
# give (an AUTH_SUPER_ADMIN_EMAILS address arrives super_admin), and refuses —
# 404 — unless NODE_ENV is development, VERCEL is unset, this is 1, the
# request's Host is localhost/127.0.0.1 with no forwarded visitor address (an
# ngrok tunnel cannot use it), and the address may sign in and is not banned.
# Needs AUTH_SECRET. Every use is recorded as `auth.dev_sign_in`.
# DEV_AUTO_SIGN_IN_EMAIL is who you become when `as` is omitted — including
# from the "Sign in as (dev)" link beside the header's Sign in control.
# DEV_AUTO_SIGN_IN=1
# DEV_AUTO_SIGN_IN_EMAIL=you@cornell.edu
# ── Rate limiting (optional) ─────────────────────────────────────────
# Upstash Redis backs the API rate limiter. When both are set, limits are
# enforced across all serverless instances. Without them, the limiter falls
# back to an in-memory store (resets on cold start; fine for basic abuse
# prevention).
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# Chat messages per hour for visitors who are NOT signed in (default 8).
# Signed-in students get 60/hour and staff/admins 200/hour; those are fixed in
# code. Raise this for a conference demo where every visitor shares one NAT'd
# IP and would otherwise exhaust a per-IP allowance in minutes.
RATE_LIMIT_ANON_CHAT=8
# ── MCP endpoint (/api/mcp) ──────────────────────────────────────────
# DEPRECATED (MCP access spec §5.3) — leave unset. /api/mcp is open for the
# public read-only tools, and callers act as themselves with a personal access
# token (profile menu → Connect an AI assistant) or by signing in with OAuth at
# /api/mcp/signed-in. For one release a request bearing this value is still
# accepted, as the public read-only identity only, with a warning in the logs
# and on /admin. It will be removed.
MCP_TOKEN=