
appconfig-system
Expert guidance for working with the AppConfig runtime configuration system in squareone. Use this s
AppConfig System
The squareone app uses a filesystem-based configuration system that replaces next/config for runtime configuration.
Critical Rules
NEVER use next/config or getConfig() - The app has been migrated away from this pattern. Always use the AppConfig system instead.
Configuration Architecture
Configuration Files
squareone.config.yaml- Public runtime configuration (accessible client-side)squareone.serverconfig.yaml- Server-only configuration (secrets, etc.)squareone.config.schema.json- JSON schema for public config validationsquareone.serverconfig.schema.json- JSON schema for server config validation
See reference/config-reference.md for complete schema documentation.
Key Modules
src/lib/config/loader.ts- Server-side configuration and MDX loadingsrc/contexts/AppConfigContext.tsx- React context for client-side access
Server-Side Configuration Loading
In getServerSideProps
Use loadAppConfig() to load configuration in getServerSideProps:
import type { GetServerSideProps } from 'next';
import { loadAppConfig } from '../lib/config/loader';
export const getServerSideProps: GetServerSideProps = async () => {
try {
// Load app configuration
const appConfig = await loadAppConfig();
return {
props: {
appConfig, // Passed to page component and extracted by _app.tsx
},
};
} catch (error) {
throw error;
}
};
See templates/page-with-config.tsx for a complete example.
Loading MDX Content
For pages that render MDX content, use loadConfigAndMdx():
import { loadConfigAndMdx } from '../lib/config/loader';
import { serialize } from 'next-mdx-remote/serialize';
export const getServerSideProps: GetServerSideProps = async () => {
try {
// Load both config and raw MDX content
const { config: appConfig, mdxContent } = await loadConfigAndMdx('docs.mdx');
// Serialize MDX for rendering
const mdxSource = await serialize(mdxContent);
return {
props: {
appConfig,
mdxSource,
},
};
} catch (error) {
throw error;
}
};
MDX Directory Configuration
- Development: MDX files in
src/content/pages/(relative path in config) - Production: Configurable via
mdxDirin YAML (absolute path for Kubernetes ConfigMaps) - Path resolution: Automatic handling of relative vs absolute paths in loader
Client-Side Configuration Access
Using the useAppConfig Hook
Components access configuration via the useAppConfig() hook:
import { useAppConfig } from '../contexts/AppConfigContext';
function MyComponent() {
const config = useAppConfig();
return (
<div>
<h1>{config.siteName}</h1>
<p>Environment: {config.environmentName}</p>
<a href={config.docsBaseUrl}>Documentation</a>
</div>
);
}
See templates/component-with-config.tsx for a complete example.
Requirements
- Component must be within
<AppConfigProvider>(automatically set up in_app.tsx) - Page must implement
getServerSidePropsto passappConfigprop - Hook throws error if used outside provider
Sentry Configuration
Server-Side (sentry.server.config.js)
Sentry configuration is loaded from environment variables and injected into AppConfig:
// In loadAppConfig():
const sentryDsn = process.env.SENTRY_DSN;
const config = {
...publicConfig,
...serverConfig,
} as AppConfig;
// Only add sentryDsn if it's defined
if (sentryDsn) {
config.sentryDsn = sentryDsn;
}
Client-Side (instrumentation-client.js)
Sentry configuration is injected into the browser via window.__SENTRY_CONFIG__ in _document.tsx.
Critical requirement: Pages MUST implement getServerSideProps to enable configuration injection. Statically rendered pages get the default configuration which disables client-side Sentry reporting.
Configuration Schema and Validation
Ajv-Based Validation
Configuration is validated using Ajv with:
- Default values - Schema defaults are applied automatically
- Additional property removal - Unknown properties are stripped
- Type validation - Ensures correct types for all fields
const ajv = new Ajv({ useDefaults: true, removeAdditional: true });
const validate = ajv.compile(schema);
// Validation modifies the configuration data
const isValid = validate(data);
if (!isValid && validate.errors) {
throw new Error(
`Configuration validation failed: ${ajv.errorsText(validate.errors)}`
);
}
Environment Variable Override
Some configurations can be overridden via environment variables:
SQUAREONE_CONFIG_PATH- Override public config file pathSQUAREONE_SERVER_CONFIG_PATH- Override server config file pathSENTRY_DSN- Sentry Data Source Name (injected at runtime)SQUAREONE_ENABLE_CACHING- Force caching in development
Caching Behavior
Production Caching
In production (NODE_ENV === 'production'), configuration and MDX content are cached:
- Config loaded once and cached module-level
- MDX content cached per-file
- Improves performance by avoiding repeated filesystem reads
Development Mode
In development, caching is disabled by default:
- Allows editing config and MDX files without restart
- Can be enabled via
SQUAREONE_ENABLE_CACHING=truefor testing
Kubernetes Deployment Pattern
ConfigMap Mounting
Configuration files can be mounted as Kubernetes ConfigMaps:
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: squareone-config
data:
squareone.config.yaml: |
siteName: 'Production Site'
baseUrl: 'https://example.com'
mdxDir: '/config/mdx' # Absolute path to mounted MDX content
# ... rest of config
# Deployment
volumeMounts:
- name: config
mountPath: /app/squareone.config.yaml
subPath: squareone.config.yaml
- name: mdx-content
mountPath: /config/mdx
Path Handling
The loader automatically handles path resolution:
- Relative path (development): Resolved from
process.cwd() - Absolute path (production): Used as-is for ConfigMap mounts
Key Benefits
- Kubernetes-ready: Configuration via ConfigMaps at runtime
- No hydration issues: No
next/configorgetInitialPropsdependencies - Type-safe: Full TypeScript support with
AppConfiginterface - Environment-agnostic: Same system works in development and production
- Content management: MDX files separate from configuration, easier to edit
Migration from next/config
If you encounter code using next/config:
Old pattern (DO NOT USE):
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
const siteName = publicRuntimeConfig.siteName;
New pattern (USE THIS):
// In getServerSideProps
import { loadAppConfig } from '../lib/config/loader';
const appConfig = await loadAppConfig();
// In components
import { useAppConfig } from '../contexts/AppConfigContext';
const config = useAppConfig();
const siteName = config.siteName;
Troubleshooting
Error: "useAppConfig must be used within an AppConfigProvider"
Cause: Component is not wrapped in AppConfigProvider or page didn't implement getServerSideProps.
Solution:
- Ensure page implements
getServerSidePropswithloadAppConfig() - Return
appConfigin props _app.tsxautomatically wraps pages withAppConfigProvider
Error: "Configuration validation failed"
Cause: YAML configuration doesn't match JSON schema.
Solution: Check schema in squareone.config.schema.json and ensure all required fields are present with correct types.
MDX file not found error
Cause: mdxDir configuration doesn't point to correct location.
Solution:
- Development: Use relative path like
src/content/pages - Production: Use absolute path like
/config/mdx(for ConfigMap mounts)
Sentry not initializing on client
Cause: Page is statically rendered (no getServerSideProps).
Solution: Add getServerSideProps to the page to enable server-side rendering and configuration injection.
API Routes
API routes can also access configuration:
import type { NextApiRequest, NextApiResponse } from 'next';
import { loadAppConfig } from '../lib/config/loader';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const config = await loadAppConfig();
// Use config...
res.status(200).json({ siteName: config.siteName });
}
Storybook Configuration
Storybook uses AppConfigProvider decorator with mock configuration:
// .storybook/preview.tsx
import { AppConfigProvider } from '../src/contexts/AppConfigContext';
const mockConfig = {
siteName: 'Storybook',
// ... mock config values
};
export const decorators = [
(Story) => (
<AppConfigProvider config={mockConfig}>
<Story />
</AppConfigProvider>
),
];
This allows components using useAppConfig() to work in Storybook stories.
Environment Variables Policy
Avoid NEXT_PUBLIC_ environment variables for runtime config - use YAML files instead.
Use environment variables only for:
- Infrastructure concerns (Sentry DSN, database URLs)
- Build-time configuration
- Secrets that shouldn't be in version control
Runtime application configuration should be in YAML files so it can be managed via Kubernetes ConfigMaps without rebuilding the application.