// Expert guidance for working with the AppConfig runtime configuration system in squareone. Use this skill when implementing configuration loading, working with YAML config files, setting up new pages that need configuration, troubleshooting config hydration issues, or migrating from next/config patterns. Covers server-side loadAppConfig(), client-side useAppConfig(), MDX content loading, Sentry configuration injection, and Kubernetes ConfigMap patterns.
| name | appconfig-system |
| description | Expert guidance for working with the AppConfig runtime configuration system in squareone. Use this skill when implementing configuration loading, working with YAML config files, setting up new pages that need configuration, troubleshooting config hydration issues, or migrating from next/config patterns. Covers server-side loadAppConfig(), client-side useAppConfig(), MDX content loading, Sentry configuration injection, and Kubernetes ConfigMap patterns. |
The squareone app uses a filesystem-based configuration system that replaces next/config for runtime configuration.
NEVER use next/config or getConfig() - The app has been migrated away from this pattern. Always use the AppConfig system instead.
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 validationSee reference/config-reference.md for complete schema documentation.
src/lib/config/loader.ts - Server-side configuration and MDX loadingsrc/contexts/AppConfigContext.tsx - React context for client-side accessUse 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.
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;
}
};
src/content/pages/ (relative path in config)mdxDir in YAML (absolute path for Kubernetes ConfigMaps)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.
<AppConfigProvider> (automatically set up in _app.tsx)getServerSideProps to pass appConfig propSentry 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;
}
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 is validated using Ajv with:
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)}`
);
}
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 developmentIn production (NODE_ENV === 'production'), configuration and MDX content are cached:
In development, caching is disabled by default:
SQUAREONE_ENABLE_CACHING=true for testingConfiguration 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
The loader automatically handles path resolution:
process.cwd()next/config or getInitialProps dependenciesAppConfig interfaceIf 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;
Cause: Component is not wrapped in AppConfigProvider or page didn't implement getServerSideProps.
Solution:
getServerSideProps with loadAppConfig()appConfig in props_app.tsx automatically wraps pages with AppConfigProviderCause: 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.
Cause: mdxDir configuration doesn't point to correct location.
Solution:
src/content/pages/config/mdx (for ConfigMap mounts)Cause: Page is statically rendered (no getServerSideProps).
Solution: Add getServerSideProps to the page to enable server-side rendering and configuration injection.
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 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.
Avoid NEXT_PUBLIC_ environment variables for runtime config - use YAML files instead.
Use environment variables only for:
Runtime application configuration should be in YAML files so it can be managed via Kubernetes ConfigMaps without rebuilding the application.