Environment & Configuration Constants
The src/config directory is the single source of truth for runtime configuration and shared application constants. src/config/env.ts funnels every environment-variable read through one module; the src/config/constants/ tree holds upload limits, feed sizes, navigation, metadata, and domain-specific invariants, all importable via the @/config barrel.
Overview
Section titled “Overview”Next.js traditionally scatters process.env.X reads across server actions, route handlers, and components — hard to audit, easy to misconfigure. src/config/env.ts resolves this with three design decisions:
- Static references for inlining.
NEXT_PUBLIC_*values are only substituted into the browser bundle when the reference is a literalprocess.env.NEXT_PUBLIC_FOOexpression. Destructuringprocess.envor computed key access breaks that substitution silently. Every variable is therefore hoisted into a module-levelconstwith a dotted literal access path. - Fail-fast on required values. The two Supabase variables are guarded immediately after the hoist block with
if (!X) throw. Missing them fails at import time with a named error, not silently at the Supabase call. - Explicit optionality. Every non-required variable gets a
|| ""fallback or an explicit default, so the exportedenvobject has a stable, non-undefinedshape — exceptstorageUrl, which is intentionallystring | undefinedand requires callers to handle the absent case themselves.
Environment (env.ts)
Section titled “Environment (env.ts)”The exported env object groups configuration into vendor namespaces and flat application-level fields:
| Field path | Source variable | Default | Notes |
|---|---|---|---|
supabase.url |
NEXT_PUBLIC_SUPABASE_URL |
required (throws) | Browser-visible Supabase project URL |
supabase.pubKey |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
required (throws) | Publishable (anon) key; safe in the browser |
google.clientId |
NEXT_PUBLIC_GOOGLE_CLIENT_ID |
"" |
Public OAuth client id for Google sign-in |
baseUrl |
NEXT_PUBLIC_BASE_URL |
"http://localhost:3000" |
Canonical origin; used for absolute links |
storageUrl |
NEXT_PUBLIC_STORAGE_URL |
undefined |
Asset/object storage origin — the only field with no fallback |
features.notifications |
NEXT_PUBLIC_FEATURE_NOTIFICATIONS |
false |
Short-lived release toggle; true only when the variable is exactly "true". Gates NotificationBell in the topbar and the /posts/[id] route. Notifications is in progress. |
resend.apiKey |
RESEND_API_KEY |
"" |
Server-only email provider credential |
resend.email |
RESEND_SENDER_EMAIL |
"" |
From-address for outgoing mail |
mailchimp.apiKey |
MAILCHIMP_API_KEY |
"" |
Server-only marketing credential |
mailchimp.audienceId |
MAILCHIMP_AUDIENCE_ID |
"" |
Target list/audience identifier |
openai.apiKey |
OPENAI_API_KEY |
"" |
Server-only; used for content moderation only |
accessToken |
ACCESS_TOKEN |
"" |
Sign-up gate token: signup() in lib/supabase/actions.ts compares the form’s access_token field against this value to gate registration |
isDevelopment |
NODE_ENV |
derived | true only when NODE_ENV === "development" |
isProduction |
NODE_ENV |
derived | true only when NODE_ENV === "production" |
SUPABASE_SERVICE_ROLE_KEY is deliberately excluded from the env object. It is read only in lib/supabase/admin.ts via a direct process.env read with a ! assertion. Excluding it from the shared object prevents it from being accidentally imported into a bundle.
Three Supabase clients (lib/supabase/admin.ts, lib/supabase/middleware.ts, lib/supabase/public.ts) bypass env and read process.env directly — a consistency note for anyone renaming variables.
Constants
Section titled “Constants”All constant modules under src/config/constants/ are importable via @/config (the barrel at src/config/index.ts). Three modules are imported by direct path only (articles.ts, organizations.ts, profile.ts) because they are consumed exclusively by their validation-adjacent call sites.
| Module | Purpose |
|---|---|
feeds.ts |
Feed and search page-size limits; SEARCH_AUTHOR_MATCH_CAP bounds the in.(…) filter list in search URLs |
posts.ts |
MAX_MESSAGE_LENGTH (3000 chars) — enforced by the post composer and MobileComposer |
password.ts |
PASSWORD_REQUIREMENTS object passed to validator.isStrongPassword, plus validatePassword and checkPasswordStrength helpers |
privacy.ts |
VISIBILITY_OPTIONS array and ContentVisibility union ("public" | "connections" | "private"). The connections option depends on Connections & blocking, which is on the roadmap — VISIBILITY_OPTIONS currently has no UI consumers |
image.ts |
IMAGE_CONFIG (allowed types, MIME types, per-slot size caps), IMAGE_ERROR_MESSAGES, and MAX_IMAGES_PER_POST — the most widely imported constant in the codebase |
documents.ts |
DOCUMENT_CONFIG (extensions, 25 MB cap) and DOCUMENT_ERROR_MESSAGES for the project document upload path |
attachments.ts |
ATTACHMENT_LIMITS (per-kind and combined caps), EDIT_GRACE_DAYS (7 days post-publication), and MIME/extension lists per kind (pdf, annex, image) |
categories.ts |
CATEGORY_DOT_COLORS (slug → Tailwind class map) and chip border constants. CATEGORY_DOT_COLORS currently has no UI consumers — exported but unread |
metadata.ts |
NO_INDEX_ROBOTS and AUTHOR_OZEAON — typed against Next.js’s Metadata type with satisfies |
navigation.ts |
DASHBOARD_NAV_ITEMS keyed by active-account type; drives the dashboard sidebar, account dropdown, and mobile profile menu |
articles.ts |
VALIDATION_MESSAGES, ARTICLE_FIELD_LIMITS, ISO_LANGUAGE_CODES, and DELETE_ARTICLE_DIALOG. ARTICLE_TYPE_REQUIRED_FIELDS is defined but has no consumers |
moderation.ts |
Model name, timeout, retry delay, and report URL for the OpenAI moderation client |
notifications.ts |
Batch size and debounce for the notification poller |
comments.ts |
Max comment length, batch size, and reply fold/expand thresholds |
postgres.ts |
PG_ERROR_CODES map for typed Postgres error handling |
Layout Constants
Section titled “Layout Constants”src/components/ui/layout/constants.ts lives outside the @/config barrel but is part of the configuration layer. It exports CONTENT_MAX_WIDTH_CLASS, CONTENT_GUTTER_CLASS, STICKY_ASIDE_CLASS, and STICKY_NAV_CLASS — Tailwind class fragments shared by the reader and feed grids so column widths and sticky offsets cannot drift independently. The STICKY_ASIDE_CLASS vs. STICKY_NAV_CLASS distinction encodes a geometry choice: max-h-… for sidebars with h-fit content vs. h-… for rails claiming the full remaining height. Both use svh (small viewport height) to stay stable across mobile browser toolbar changes.
Root Modules Outside the Barrel
Section titled “Root Modules Outside the Barrel”src/config/connectionConfig.ts encodes connection-request expiration (30 min) and cancel cooldown (30 min) and exports two helpers that turn those windows into ISO timestamp strings. Its sole consumer is lib/supabase/queries/profile.ts. Connections & blocking are on the roadmap — this config backs dormant code; ProfileActions (ConnectButton/BlockButton) is not rendered.
src/config/reaction-types.ts exports getReactionTypeId(slug, supabase), which resolves a reaction slug to its database id using a module-level cache. The cache is never invalidated, so reaction types added after process start are invisible until restart. Consumers: app/api/posts/[id]/like/route.ts and lib/supabase/queries/reactions.ts.
Failure Modes & Edge Cases
Section titled “Failure Modes & Edge Cases”Import-time failure is global. Missing a required Supabase variable throws at first import — including during build. There is no partial-degradation path for either required field.
Client bundle leakage. Nothing in the type system distinguishes public from server-only fields on the env object. Importing env into a client component returns "" for server-only keys (they are not substituted, so undefined is caught by the || "" fallback). This masks bugs rather than leaking secrets.
storageUrl is the only unguarded export. It is string | undefined; every other string field is guaranteed non-undefined.
Silent degradation on optional secrets. A missing RESEND_API_KEY or OPENAI_API_KEY starts the process successfully and fails only when the feature is used. Features relying on server-only keys must validate at their own boundary.
reaction-types.ts cache staleness. The module-level cache is never cleared. Reaction types added while the process is running are invisible until restart — a deliberate trade-off (one DB round-trip per process lifetime).
Operational Notes
Section titled “Operational Notes”- The
envobject is built once at module evaluation and never mutated — no I/O, no lazy re-read, no synchronization required. - Adding a variable: hoist a literal
const X = process.env.Xreference; choose a tier (throw,|| "", explicit default, or derived boolean); add it to the appropriate vendor group or flat field. features.notificationsis explicitly short-lived: when the last notifications ticket closes, remove the constant, the hoist, and the env variable together.- Prefer
@/configfor all new constant imports. Direct-path imports (@/config/constants/articles) are for modules consumed only from their own domain.
Extension Points
Section titled “Extension Points”- Add a vendor group by extending the
envliteral with a new namespaced key and adding the corresponding hoistedconsts. - Additional Tailwind class fragments shared by more than one component belong in
src/components/ui/layout/constants.ts. - The three Supabase clients that bypass
envcould be routed through a validated superset;SUPABASE_SERVICE_ROLE_KEYwould need a separate admin-only accessor to preserve isolation.
Related Links
Section titled “Related Links”src/config/env.ts— environment aggregation modulesrc/config/index.ts—@/configbarrelsrc/config/constants/— all constant modules- Logging & Observability —
src/lib/logger/config.tsand instrumentation setup - Zod Schemas — Zod schemas that consume
articles.tslimits and messages - Media & Images —
IMAGE_CONFIGconsumers