Plugin configuration¶
Use a plugin configuration schema when a plugin needs installation-specific
values. It defines the contract between the distribution manifest, the backend
process, and, for explicitly safe values, the frontend. A plugin reads neither
global Django settings nor os.environ directly.
Audience, prerequisites, and outcome¶
This guide is for authors of a backend or full-stack plugin that is selected in a distribution. You need a descriptor and a Python package as described in plugin layouts.
The outcome is a frozen, namespaced Pydantic configuration object that rejects unknown fields, resolves secret references once during backend startup, and supplies a deliberately small JSON-safe projection to frontend code.
Declare a schema on the descriptor¶
Subclass PluginConfigSchema. Its Pydantic model is frozen and uses
extra='forbid', so a misspelled or unsupported manifest field fails before a
plugin serves requests. Use ordinary field types for non-secret values. A field
that can contain a secret must include SecretRef in its type.
from atlas_plugin_api import PluginConfigSchema, PluginDescriptor, SecretRef
class InventoryConfig(PluginConfigSchema):
api_base_url: str
refresh_minutes: int = 15
api_token: str | SecretRef
PUBLIC_FIELDS = frozenset({"api_base_url", "refresh_minutes"})
PLUGIN = PluginDescriptor(
id="atlas.inventory",
version="0.1.0",
compatibility={"atlasCore": ">=0.1 <1"},
django_apps=("atlas_plugin_inventory",),
entry_point="atlas_plugin_inventory.plugin:PLUGIN",
config_schema=InventoryConfig,
)
Keep the schema in the plugin's public package, close to its descriptor. Do not share an unnamespaced settings model between plugins: each selected plugin receives only its own resolved schema instance. For the exact descriptor type, see the plugin contract reference.
Configure it in a distribution¶
The manifest carries a config block under the matching plugin entry. Values
are validated against InventoryConfig while the distribution is composed.
For a real credential, store the value in the deployment environment and put
only the environment-variable name in source control:
plugins:
- id: atlas.inventory
version: 0.1.0
backend: { package: atlas-plugin-inventory, source: workspace }
config:
api_base_url: https://inventory.example.internal
refresh_minutes: 10
api_token: { fromEnv: INVENTORY_API_TOKEN }
{fromEnv: INVENTORY_API_TOKEN} is a SecretRef, not the secret itself. Do
not put a production token in a manifest, lock file, checked-in example, or
frontend configuration. A literal is accepted only for a field whose schema
also accepts it; use that only for an intentionally non-secret development
value.
Validate the selected distribution before building it:
uv run --project composer atlas-compose resolve distributions/default/manifest.yaml -o distributions/default/lock.yaml
uv run --project composer atlas-compose validate distributions/default/manifest.yaml distributions/default/lock.yaml
See composition errors when schema validation rejects a field, type, or plugin selection.
Runtime resolution and backend use¶
Core validates the raw block during settings setup, resolves every SecretRef
once against the process environment, and stores the resulting object in the
central plugin configuration registry. It does not write the resolved value to
the manifest, lock, generated composition modules, or a response.
Read the typed instance through the registry after startup. Treat an absent entry as a configuration or selection error:
from server.apps.plugins.config import registry
config = registry.get("atlas.inventory")
if config is None:
raise RuntimeError("atlas.inventory configuration was not registered")
assert isinstance(config, InventoryConfig)
client = InventoryClient(base_url=config.api_base_url, token=config.api_token)
MissingSecretEnvError names both the configuration field and the missing
environment variable. Set that variable in the backend process environment and
restart. Never log the secret value or replace the reference with a committed
literal. Centralized resolution keeps plugin code from accidentally serializing
or leaking secrets.
Expose a public frontend projection¶
Nothing crosses to the browser by default. PUBLIC_FIELDS is an allow-list;
the backend calls public_projection() for every registered plugin and sends
only those fields in the public bootstrap configuration, keyed by plugin id.
PluginConfigSchema rejects a PUBLIC_FIELDS entry that is unknown or whose
type can hold SecretRef, even if the current value happens to be harmless.
The matching TypeScript types are intentionally JSON-only:
import type { BootstrapConfig, PluginPublicConfig } from '@atlas/plugin-api'
function inventoryConfig(config: BootstrapConfig): PluginPublicConfig | undefined {
return config['atlas.inventory']
}
Treat this as display and UI-behavior configuration, not authorization or a secret transport. The frontend must not infer permission from it; the backend still enforces every protected action. See frontend contributions for the separate route and UI-composition contract.
Validate the contract¶
Add focused tests beside the schema for unknown fields, fromEnv resolution,
missing environment variables, and the public projection. The repository's
contract tests exercise the same boundary:
cd core/backend
uv run pytest \
../../plugin-api/python/atlas_plugin_api/tests/test_config.py \
server/apps/plugins/tests/test_config.py -q
Failures and safe corrections¶
| Symptom | Cause | Safe correction |
|---|---|---|
| Manifest validation rejects a field | The name is unknown, its value has the wrong type, or the plugin has no matching schema. | Correct the namespaced config block or evolve the schema deliberately; do not ignore unknown fields. |
Startup reports MissingSecretEnvError |
The fromEnv name is absent from the backend process environment. |
Supply the named variable through the deployment secret mechanism and restart. |
| A secret appears in a browser response | A secret-capable field was incorrectly treated as public. | Remove it from PUBLIC_FIELDS; the schema should also type it as … | SecretRef so projection fails safely. |
| Frontend config is empty | The field was not explicitly listed in PUBLIC_FIELDS, or the plugin was not registered. |
Expose only the non-sensitive value required by UI code and verify the plugin is selected. |
Plugin code reads os.environ |
Configuration is bypassing validation and centralized resolution. | Move the value into the plugin schema and retrieve the resolved instance from the registry. |
Next, use permissions to protect any action that uses this configuration, backend collaboration for cross-plugin contracts, and the environment reference for Atlas-wide operator settings.