docs/configuration-provisioning.md
Status: Implemented
Memos should follow Mastodon's deployment-configuration model: configuration supplied by the deployment is loaded directly into each server process and remains authoritative for that process lifetime. It is not imported into the database and is not tracked as database-owned application state.
The first supported file-backed configuration resources are:
Memos scans /etc/secrets after database migration and demo seeding, validates every matching file, builds one immutable configuration snapshot, and
publishes that snapshot before HTTP or background services start. Applying a changed file requires a process restart.
Every resource file contains exactly one existing memos.store protobuf message encoded as protobuf JSON. No resource envelope, state file, ownership
table, or second persistent copy of a secret is introduced. The process necessarily holds decoded secrets in its private runtime snapshot.
Mastodon reads external authentication and other deployment configuration from environment variables or a dotenv file during process initialization. It does not copy that configuration into an administrator-editable database resource or maintain Terraform-style ownership state.
Memos should use the same lifecycle while adapting the input format to its existing generated store messages:
This is deployment configuration, not resource reconciliation. Terms such as adoption, import, unmanage, drift, prune, and Terraform state do not apply.
provisioning_resource table or provisioning columns to existing tables.Stored configuration
: Configuration stored in the existing idp and system_setting database tables.
Deployment configuration : Configuration decoded from matching files during process startup.
Effective configuration : The configuration used by APIs, authentication, and background services. Deployment configuration shadows stored configuration with the same stable key.
Stable key : The identity-provider UID or instance-setting key used to merge deployment and stored configuration.
Memos scans direct children of /etc/secrets. The directory may contain unrelated platform secrets; only supported filename patterns are read. Memos does
not recurse into subdirectories and does not create, modify, or delete anything in the directory.
| Filename pattern | Protobuf message | Stable key |
|---|---|---|
memos-idp-<label>.json | memos.store.IdentityProvider | uid |
memos-instance-setting-<label>.json | memos.store.InstanceSetting | key |
<label> uses lowercase kebab case and must match [a-z0-9]+(?:-[a-z0-9]+)*. Matching is case-sensitive and the extension is lowercase .json.
For upgrade compatibility, identity-provider filenames accepted by the original bootstrap (memos-idp-*.json) continue to load when the label is not
lowercase kebab case, but startup logs a deprecation warning. New files should always use the canonical convention.
Recommended labels mirror the resource key for operator readability:
| Resource | Canonical filename |
|---|---|
Identity provider with UID primary-sso | memos-idp-primary-sso.json |
GENERAL | memos-instance-setting-general.json |
STORAGE | memos-instance-setting-storage.json |
MEMO_RELATED | memos-instance-setting-memo-related.json |
NOTIFICATION | memos-instance-setting-notification.json |
AI | memos-instance-setting-ai.json |
The filename label remains descriptive rather than authoritative. The uid or key inside the message is the resource identity, so renaming a file does
not change account links or effective resource identity. Files are read in lexical order only to produce deterministic diagnostics; ordering has no
configuration semantics.
Each matching file:
A missing directory or a readable directory without matching files is a normal no-op. An unreadable directory or matching file is a startup error. Startup
logs include matched counts by resource type so a misspelled filename is visible without logging file contents. A direct child beginning with memos- but
not matching a supported pattern produces a warning; unrelated filenames are silently ignored.
An SSO-only deployment mounts both an identity-provider file and memos-instance-setting-general.json with disallowPasswordAuth enabled. These resources
are validated and published together during startup. The public demo seed does not contain authentication policy, so both files must be mounted to enable
SSO-only behavior. Keep disallowUserRegistration disabled when first-time SSO users should be created automatically.
An identity-provider file contains exactly one memos.store.IdentityProvider. The database-generated id must be omitted. uid is required and is the
stable key.
Example /etc/secrets/memos-idp-primary-sso.json:
{
"uid": "primary-sso",
"name": "Company SSO",
"type": "OAUTH2",
"identifierFilter": "",
"config": {
"oauth2Config": {
"clientId": "client-id",
"clientSecret": "client-secret",
"authUrl": "https://idp.example.com/oauth/authorize",
"tokenUrl": "https://idp.example.com/oauth/token",
"userInfoUrl": "https://idp.example.com/oauth/userinfo",
"scopes": ["openid", "profile", "email"],
"fieldMapping": {
"identifier": "sub",
"displayName": "name",
"email": "email",
"avatarUrl": "picture"
}
}
}
}
Initial validation supports only OAuth2 providers and requires:
Duplicate UIDs across files are rejected.
User identity links already use the provider UID as their stable provider value. A file-backed provider therefore does not need a database-generated IdP ID to preserve account links or complete SSO sign-in.
An instance-setting file contains exactly one memos.store.InstanceSetting. key is required and is the stable key. The populated oneof must match the
key.
Example /etc/secrets/memos-instance-setting-general.json:
{
"key": "GENERAL",
"generalSetting": {
"disallowUserRegistration": false,
"disallowPasswordAuth": true,
"additionalScript": "",
"additionalStyle": "",
"weekStartDayOffset": 1,
"disallowChangeUsername": false,
"disallowChangeNickname": false,
"customProfile": {
"title": "Company Memos",
"description": "Internal notes",
"logoUrl": "https://example.com/logo.png"
}
}
}
Example /etc/secrets/memos-instance-setting-notification.json:
{
"key": "NOTIFICATION",
"notificationSetting": {
"email": {
"enabled": true,
"smtpHost": "smtp.example.com",
"smtpPort": 587,
"smtpUsername": "memos",
"smtpPassword": "smtp-secret",
"fromEmail": "[email protected]",
"fromName": "Memos",
"replyTo": "[email protected]",
"useTls": true,
"useSsl": false
}
}
}
Supported keys:
| Key | Deployment use |
|---|---|
GENERAL | Registration, authentication, branding, scripts, styles, and user-profile policy |
STORAGE | Attachment storage type, limits, paths, and S3 credentials |
MEMO_RELATED | Memo limits, editing behavior, and reactions |
NOTIFICATION | SMTP transport and credentials |
AI | AI providers, API keys, and transcription defaults |
Rejected keys:
BASIC contains the instance secret key and database schema version. Replacing the secret key invalidates sessions, while replacing the schema version
interferes with database migration state.TAGS is retained for backward compatibility; active tag metadata is stored per user.Only one file may declare a given setting key.
An instance-setting group is the smallest deployment-configured unit. A file replaces the complete effective group. A scalar omitted from protobuf JSON is stored in the decoded message as its protobuf default; omission does not preserve a field from the database value.
Some existing setting getters apply application defaults after decoding zero values. For example, STORAGE defaults to local storage, a 30 MiB upload limit,
and assets/{timestamp}_{uuid}_{filename} when the corresponding decoded fields are unspecified. The effective behavior is therefore the decoded file plus
the same read-time defaults used for database-backed configuration.
Empty secret fields in a file mean empty values; they never mean "preserve the database secret." Credential-preservation behavior used by UI updates does not apply to deployment configuration.
AI deployment configuration uses deterministic, self-contained normalization rather than the UI update path:
id; the loader never generates one.https://api.openai.com/v1.https://generativelanguage.googleapis.com/v1beta.Using store protobuf JSON makes the selected messages a supported deployment-configuration interface even though the messages remain internal to the application. For every provisionable message, Memos must preserve:
oneof mappings.New optional fields and enum values may be added. A provisionable field may be deprecated, but its existing JSON spelling must continue to decode for the supported upgrade window. Field names must not be reused with a different meaning. New validation should not invalidate an existing safe configuration without an upgrade note and a documented replacement.
Unknown fields remain startup errors because this catches misspellings and configuration written for a newer, incompatible Memos version. Compatibility tests should keep representative JSON fixtures from earlier releases and decode them with the current loader.
The loader builds an immutable snapshot containing maps keyed by provider UID and setting key. It does not mutate the database while loading. The Store
owns this snapshot so all existing consumers resolve configuration through one boundary.
Startup follows this sequence:
Initialize or migrate database
-> apply demo seed when enabled
-> read all matching deployment-configuration files
-> decode and validate every resource
-> validate affected cross-resource invariants
-> publish one immutable runtime snapshot
-> construct HTTP and background services
-> accept requests
If any matching file is invalid, no snapshot is published and startup fails. Atomicity comes from publishing the snapshot only after complete validation; no cross-database transaction abstraction is required because deployment configuration performs no database writes.
The snapshot is loaded once. Files changed after startup have no effect until the process restarts.
Generated protobuf messages are mutable pointers, so immutability must be enforced rather than assumed:
Store.proto.Clone, rather than canonical pointers.This prevents one request, background runner, defaulting helper, or redaction path from changing configuration observed by another goroutine.
The store facade has an explicit separation between effective reads and stored-resource access:
List and get operations return the union of stored and file-backed providers by UID:
Operators migrating an existing stored provider to a file should keep the same UID so existing user-identity links continue to work. They should remove or update the shadowed stored provider before later removing the file if they do not want the old database configuration to reappear.
Every effective instance-setting getter checks the runtime snapshot before its database cache:
system_setting row with the same key.The demo seed writes MEMO_RELATED but does not write GENERAL. Loading deployment configuration after seeding supplies the complete effective General
settings without embedding deployment authentication policy in demo data.
All file-local validation runs before snapshot publication. Relationships between file-backed resources are validated against the resulting effective configuration when the desired files affect that relationship.
At minimum, validation rejects:
GENERAL setting that disables password authentication for regular users when the resulting effective configuration has no identity
provider.oneof does not match the key.An unrelated file must not turn an existing database condition into a new startup failure. For example, a STORAGE-only file does not fail startup merely because the database already disables regular-user password sign-in while containing no IdP; Memos logs that existing condition as a warning. A file that configures GENERAL or an IdP evaluates the authentication invariant against the resulting effective state.
The administrator password path remains available regardless of disallowPasswordAuth. Runtime mutations reject transitions from a safe authentication
state to one where password sign-in is disabled for regular users without an effective IdP. An unrelated edit may preserve an already-existing legacy
violation so an upgrade does not make the complete GENERAL group uneditable; the administrator can resolve that state by enabling password sign-in or
configuring an IdP. Deleting the last effective IdP from a previously safe state remains rejected.
The validation and database mutation must be one serializable store operation. In particular, updating GENERAL and deleting an IdP cannot use separate
check-then-write calls, because concurrent requests could each validate an old safe state and together produce an unsafe state. The narrow runtime mutation
operation:
GENERAL setting and stored IdPs inside that transaction.This transaction is required for runtime authentication-policy safety, not for loading deployment files. It adds no table or schema migration. Each database driver must provide equivalent transaction semantics for this narrow operation.
The API operates on effective resources for reads and stored resources for permitted writes.
Mutation behavior:
codes.FailedPrecondition.codes.FailedPrecondition.codes.FailedPrecondition.No API operation writes to the mounted files. Test operations that do not change stored configuration, such as testing the effective SMTP configuration, remain available.
The frontend does not receive configuration-source metadata. It presents the normal mutation controls and reports the API's FailedPrecondition error when
an administrator attempts to create, update, or delete a deployment-managed resource. The API remains the sole authority for mutation enforcement.
/etc/secrets and every matching file as sensitive plaintext.idp, system_setting, a state file, or ownership metadata.Every replica independently loads deployment configuration at startup, as Mastodon processes independently load environment configuration. All replicas in one deployment must mount identical files.
A rolling deployment can temporarily run old and new configuration generations at the same time. Memos does not attempt distributed reconciliation or cache invalidation for this process-local configuration. Deployments changing authentication or storage configuration should use a rollout strategy that does not route traffic to replicas with different file generations, and readiness must be reported only after the new snapshot validates successfully.
Because file-backed settings bypass the database setting cache, a replica cannot replace a deployment value with a stale cached database value.
This design requires no database schema changes and no migrations:
idp.system_setting.Versions with the original memos-idp-*.json bootstrap copied file-backed IdPs, including client secrets, into the idp table during migration. The new
loader cannot reliably distinguish those rows from providers created through the UI, so it must not delete or scrub them automatically.
When a file shadows a stored provider with the same UID, startup logs a secret-free warning that a stored copy remains. Operators who previously used the database-writing bootstrap should clean up explicitly:
Until that cleanup is complete, the old stored provider and secret remain in the database and can reappear if the file is removed. The no-persistence guarantee applies to the new loader; it does not claim to erase secrets written by earlier versions.
The implementation:
memos-instance-setting-*.json resources.codes.FailedPrecondition for rejected writes.The implementation requires tests for:
BASIC, TAGS, and key/oneof mismatches.GENERAL updates and IdP deletion preserving the runtime authentication invariant across database drivers.