Configuration files

Configuration rules, shared types, and a file index for the Saavo template.

Project configuration lives in config/. To change site information, feature switches, products, payments, or third-party services, first find the relevant configuration file, then check the schema and calling code for field requirements.

Do not store secrets in configuration files

Content in config/ is committed to Git and may also appear in client bundles or build artifacts. Passwords, API keys, access tokens, and signing secrets must be stored in environment variables or Cloudflare secrets.

Configuration files can contain only static values

Configuration settings accept only static values such as strings, numbers, booleans, arrays, and plain objects. Do not put functions, class instances, Promises, or values that require runtime computation in configuration files. Constant expressions such as 5 * 1024 * 1024, whose results are known at load time, are allowed. Logic that depends on the current request, user, database, system time, or random numbers belongs in the corresponding business code.

Reading configuration

Files in config/ are ordinary TypeScript modules. The application imports configuration constants directly, without converting them to JSON. Most configuration objects end with as const satisfies ...Input:

  • as const preserves literals such as product IDs and role names so that other code can infer precise union types from them.
  • satisfies checks field names and input types while preserving the object's exact type.
import type { WebsiteDeployCfgInput } from '@/libs/config/schemas/deploy';

export const websiteDeployCfg = {
    // Configuration settings
} as const satisfies WebsiteDeployCfgInput;

The server reads the full configuration through:

getServerConfig(): AppConfig

On the first call, the application merges all configuration and validates it with AppConfigSchema. Once validation succeeds, the result is stored in the configuration registry, and subsequent calls read it directly. If validation fails, the application reports Configuration validation failed at startup and stops startup.

The browser reads public configuration through:

getClientConfig(): typeof _rawConfig

Client configuration currently includes only base, i18n, scopes, ads, and analytics. These values are bundled for the browser and are directly visible to visitors, so they must not contain sensitive information.

Type checking and startup validation are different

The satisfies operator checks only constraints that TypeScript can express. Zod checks email formats, numeric ranges, time interval formats, and references between product and affiliate configuration at startup. After changing configuration, checking only for editor errors is not enough.

Types ending in ...Input are the input types used in configuration files, usually with satisfies. A type with the same name but without Input generally represents the result after Zod validation. The two types may differ when fields have defaults, preprocessing rules, or optional settings. Use the Input type when writing configuration. Business code reads the validated configuration through getServerConfig().

Shared types

Multiple configuration files use LocalizedText and DateIntervalType. Their fields and usage rules are described below.

LocalizedText

The LocalizedText type specifies localized text and is defined in src/libs/i18n/types.ts. It is an object with value and key fields, not a plain string.

Prop

Type

Provide at least one of value or key. For multilingual text, you usually provide both: the page first looks up the current language's text using key, then displays value if no translation is found.

{
    value: 'Saavo Starter',
    key: 'website.title',
}

DateIntervalType

The DateIntervalType type represents a duration as a string. It is commonly used for session lifetimes, cache durations, rate-limit periods, and affiliate settlement periods. The format is a number followed by a time unit, with an optional space between them.

UnitMeaningExample
msMilliseconds500ms
sSeconds30s
mMinutes15m
hHours2h
dDays7d
wWeeks2w
moMonths1mo
yYears1y

You can also write units as full English words such as seconds, minutes, and hours. A number without a unit is interpreted as milliseconds, which is easy to get wrong, so always specifying a unit is recommended. Note that m means minutes, while mo means months.

Changing configuration

Find the file responsible for the feature

Open the relevant article from the table below and check the field path, type, template default, and purpose. Do not put a setting in an unrelated file just because field names look similar.

Follow the existing structure

Keep the exported variable names and the final satisfies type constraint. For new object keys, prefer stable, readable English identifiers. Use LocalizedText for user-facing text rather than using Chinese text directly as a product ID, role name, or authorization scope.

When changing translation keys, update the JSON files for every supported language. When changing Stripe Price IDs, Cloudflare resource names, email addresses, or third-party service identifiers, also confirm on the relevant platform that the resources exist and that the account you use can access them.

Run the full checks

Run npm run lint, npx tsc --noEmit, and npm run build in order. The production build triggers content generation and the application build, helping catch configuration errors that appear only during startup or bundling.

Configuration file index

FilePurpose
base.tsSite name, SEO sharing image, social accounts, administrator emails, and email addresses
i18n.tsSupported and default site languages
deploy.tsHosts, security policies, storage, email, authentication, analytics, and content switches
payment.tsPayment provider, Checkout, and billing portal
products.tsProducts, plans, Stripe Price IDs, and roles and entitlements granted after purchase
capability.tsCapability definitions used by roles and entitlements
roles.tsRoles and their static capabilities
entitlements.tsEntitlements and the capabilities they include
scopes.tsOAuth 2.0 authorization scopes
upgrade.tsPlan upgrade suggestions on the OAuth authorization page, empty by default
affiliate.tsAffiliate program, commission, and settlement rules
analytics.tsThird-party analytics services
notifications.tsExternal notification channels
cookie-consent.tsCookie consent modes, categories, and third-party services
header-menu.tsHeader navigation
footer-menu.tsFooter navigation
ads.tsBrowser-side configuration for advertising services

At startup, the application validates configuration with the Zod schemas in src/libs/config/schemas/. TypeScript type checking catches field name and type errors. Zod also checks email formats, numeric ranges, cross-file references, and other constraints.

General considerations

  • Do not casually rename exported configuration variables. The src/libs/config/client.ts and src/libs/config/index.ts modules import them directly, so renaming them also requires updating the loading code.
  • The base.ts, i18n.ts, scopes.ts, ads.ts, and analytics.ts files are included in client configuration. Treat every field as public information. Other configuration files are also committed to the repository and must not contain secrets either.
  • Configuration files reference one another. Roles reference capabilities, products reference roles and entitlements, affiliate configuration references products and plans, and upgrade suggestions reference OAuth authorization scopes. Search the entire project before renaming or deleting definitions rather than changing only the definition itself.
  • TypeScript and Zod cannot confirm that external resources exist. Check Stripe Price IDs, Cloudflare bindings, email domains, and third-party service accounts on their respective platforms.
  • Do not hardcode environment differences in configuration files. Domains, secrets, and values that differ between development, preview, and production belong in environment variables, Cloudflare secrets, or the corresponding deployment configuration.
  • Pay attention to order when changing default arrays or objects. Some pages display menus, products, and options in configuration order. Reordering them can change what users see even when there is no type error.