One TypeScript file for all your app settings. Configure via .env files for different environments.
Two simple APIs for two contexts:
import { getConfig } from '@/config';
// ✅ Server loader/action - pass context
export function loader({ context }: LoaderFunctionArgs) {
const config = getConfig(context);
return { limit: config.global.productListing.productsPerPage };
}
// ✅ Client loader - no context needed
export function clientLoader() {
const config = getConfig();
return { limit: config.global.productListing.productsPerPage };
}import { useConfig } from '@/config';
export function MyComponent() {
const config = useConfig();
return <div>Showing {config.global.productListing.productsPerPage} products</div>;
}Override any configuration value using environment variables with the PUBLIC__ prefix, no need to modify config.server.ts.
The double underscore (__) lets you navigate nested config paths. Think of it as replacing the dot (.) in JavaScript object notation:
# This environment variable:
PUBLIC__app__site__locale=en-US
# Maps to this config path:
config.app.site.locale
# Which creates this structure:
{
app: {
site: {
locale: 'en-US'
}
}
}Copy .env.default to .env and set these required Commerce Cloud credentials:
PUBLIC__app__commerce__api__clientId=your-client-id
PUBLIC__app__commerce__api__organizationId=your-org-id
PUBLIC__app__commerce__api__siteId=your-site-id
PUBLIC__app__commerce__api__shortCode=your-short-codeValues are automatically parsed to the correct type:
PUBLIC__app__myFeature__count=42 # → number
PUBLIC__app__myFeature__enabled=true # → boolean
PUBLIC__app__myFeature__items=["a","b"] # → array
PUBLIC__app__myFeature__data='{"x":1}' # → object
PUBLIC__app__myFeature__name=hello # → string
PUBLIC__app__myFeature__value= # → empty stringYou can also set entire nested objects at once using JSON:
# Instead of setting each value separately:
PUBLIC__app__myFeature__option1=value1
PUBLIC__app__myFeature__option2=value2
PUBLIC__app__myFeature__nested__enabled=true
# Use a single JSON value:
PUBLIC__app__myFeature='{"option1":"value1","option2":"value2","nested":{"enabled":true}}'Case doesn't matter: You can use any casing (lowercase, UPPERCASE, or MixedCase), and it will normalize to match your config.server.ts:
PUBLIC__app__site__locale=en-US # ✅ Works
PUBLIC__APP__SITE__LOCALE=en-US # ✅ Also works
PUBLIC__App__Site__Locale=en-US # ✅ Also worksPaths must exist in config: You can only override paths that are already defined in config.server.ts. This prevents typos from silently failing:
PUBLIC__app__site__local=en-US # ❌ Error: "local" doesn't exist (did you mean "locale"?)More specific paths win: When paths overlap, deeper paths take precedence:
PUBLIC__app__myFeature='{"setting1":500,"setting2":1000}'
PUBLIC__app__myFeature__setting1=999 # ← This wins (more specific)
# Result: setting1=999, setting2=1000Depth limit: Paths are limited to 10 levels deep. For deeper structures, use JSON values instead:
# ❌ Too deep (11 levels):
PUBLIC__a__b__c__d__e__f__g__h__i__j__k=value
# ✅ Use JSON instead:
PUBLIC__app__myFeature='{"deep":{"nested":{"structure":{"works":"fine"}}}}'PUBLIC__ prefix → Exposed to the browser (bundled into client JavaScript)
- ✅ Use for: Client IDs, site IDs, locales, feature flags, public API endpoints
- ❌ Never use for: API secrets, passwords, private keys, authentication tokens
No prefix → Server-only (never exposed to client)
- ✅ Use for: SLAS secrets, database credentials, private tokens
# ✅ Safe to expose to client:
PUBLIC__app__commerce__api__clientId=abc123
# ✅ Server-only secret (no PUBLIC__ prefix):
COMMERCE_API_SLAS_SECRET=your-secret-hereRead server-only secrets directly from process.env in your server code—never add them to config.
Environment variables are deep merged into defaults from config.server.ts:
// config.server.ts (defaults)
export default defineConfig({
app: {
myFeature: {
debounce: 750,
maxItems: 999,
enabled: true,
}
}
});
// With env var:
// PUBLIC__app__myFeature__debounce=1000
// Final result:
{
app: {
myFeature: {
debounce: 1000, // ← overridden
maxItems: 999, // ← preserved
enabled: true, // ← preserved
}
}
}export type Config = {
app: {
myFeature: {
enabled: boolean;
maxItems: number;
};
};
};export default defineConfig({
app: {
myFeature: {
enabled: false, // Just the default - no process.env needed!
maxItems: 10,
},
},
});# No code changes needed - just use the PUBLIC__ prefix!
PUBLIC__app__myFeature__enabled=true
PUBLIC__app__myFeature__maxItems=20In React components:
import { useConfig } from '@/config';
export function MyComponent() {
const config = useConfig();
if (config.myFeature.enabled) {
const maxItems = config.myFeature.maxItems;
// Your feature code here
}
}In loaders/actions:
import { getConfig } from '@/config';
export function loader({ context }: LoaderFunctionArgs) {
const config = getConfig(context);
if (config.myFeature.enabled) {
// Your loader code here
}
}In srce/config/config-meta,json:
- Add the name and key value to the config array
- This will cause the create-storefront script to ask for user input, using the value in
.env.defaultas default value
{
"configs": [
{
"name": "API Client ID",
"key": "PUBLIC__app__commerce__api__clientId"
},
{
"name": "API Organization ID",
"key": "PUBLIC__app__commerce__api__organizationId"
},
{
"name": "API Short Code",
"key": "PUBLIC__app__commerce__api__shortCode"
}
]
}- Defaults defined in
config.server.ts(clean, noprocess.envreferences) - Environment variables with
PUBLIC__prefix are automatically merged at runtime - Final config is made available via:
getConfig(context)for loaders/actionsuseConfig()for React componentswindow.__APP_CONFIG__for client code
The .server.ts suffix prevents accidental direct imports. The PUBLIC__ prefix ensures only client-safe values are exposed.
What gets shared:
- The
appsection → Available on both server and client - The
runtimeandmetadatasections → Server-only (not injected to client)
Changed .env but nothing happened?
- Restart your dev server (environment variables are loaded at startup)
Environment variable not working?
- Verify the variable name starts with
PUBLIC__(double underscore) - Check
.envfile is in the project root - For booleans, use string
"true"not baretrue
Type errors after adding config?
- Update both
schema.tsandconfig.server.tsto match - Run
pnpm typecheckto verify all files are correct
App won't start - missing credentials?
- Copy
.env.defaultto.env - Set required Commerce Cloud credentials:
PUBLIC__app__commerce__api__clientId=your-id PUBLIC__app__commerce__api__organizationId=your-org PUBLIC__app__commerce__api__siteId=your-site PUBLIC__app__commerce__api__shortCode=your-code
When deploying to Managed Runtime, set the same environment variables in the MRT environment:
- Log into the Runtime Admin
- Navigate to your project → Environment Variables
- Add the required
PUBLIC__variables (same ones from your.envfile) - Add any server-only secrets without the
PUBLIC__prefix - Deploy your application
All the same rules apply: use the PUBLIC__ prefix for client-safe values, use the __ path syntax for nested config, and read server-only secrets directly from process.env.
MRT limits: Variable names max 512 characters, total PUBLIC__ values max 32KB. Use JSON to consolidate related settings if needed.
Learn more about MRT environment variables →
Marketing Cloud is used for sending emails in features like passwordless login and password reset. The configuration is optional and only required if you're using these features.
# Marketing Cloud API Configuration (Server-only - NO PUBLIC__ prefix)
MARKETING_CLOUD_CLIENT_ID=your-client-id
MARKETING_CLOUD_CLIENT_SECRET=your-client-secret
MARKETING_CLOUD_SUBDOMAIN=your-subdomain
MARKETING_CLOUD_PASSWORDLESS_LOGIN_TEMPLATE=your-passwordless-template-id
MARKETING_CLOUD_RESET_PASSWORD_TEMPLATE=your-reset-password-template-idImportant Security Notes:
- ❌ These variables do NOT have the
PUBLIC__prefix - they are server-only - ❌ They are NOT included in
config.server.tsor exposed to the client - ✅ Read them directly from
process.envin server-side code