toeverything/AFFiNE · error · Error
App config paths must not overlap
Error message
App config paths must not overlap: ${overlappingKey} and ${key} What it means
Thrown by AppConfigModel.save() in packages/backend/server/src/models/config.ts. App config keys are dot-separated paths forming a tree ('a' is the parent of 'a.b'), and save() upserts each update by id, so writing both a prefix and its descendant would make the stored config ambiguous. Under a pg_advisory_xact_lock, save() loads all existing app_config ids and rejects the whole batch when any update key overlaps a stored id or an earlier key in the same request (candidate.startsWith(`${key}.`) or key.startsWith(`${candidate}.`)). No upsert runs after the throw.
Solutions
- Remove one of the overlapping keys from the updates array - store only leaf paths so every key is unambiguous
- Load the stored keys first with appConfigModel.load() and drop any update whose key prefixes or is prefixed by an existing id
- Delete or migrate the conflicting app_config row, then retry the save
- If hierarchical writes are needed, ensure no key in the batch is a dotted ancestor of another (earlier keys are compared against later ones via updateKeys.slice(0, index))
Example fix
// before
await config.save(user, [
{ key: 'flags', value: { copilot: true } },
{ key: 'flags.copilot', value: true }, // overlaps 'flags'
]);
// after - leaf keys only
await config.save(user, [
{ key: 'flags.copilot', value: true },
]); Defensive patterns
Strategy: validation
Validate before calling
const keysOverlap = (a: string, b: string) =>
a !== b && (a.startsWith(`${b}.`) || b.startsWith(`${a}.`));
// before save(): check within the batch and against stored keys
const existing = (await config.load()).map(c => c.id);
const bad = updates.filter(u =>
existing.some(id => keysOverlap(id, u.key)) ||
updates.some(o => o !== u && keysOverlap(o.key, u.key))
);
if (bad.length) {
throw new Error(`Overlapping config keys: ${bad.map(b => b.key).join(', ')}`);
}
await config.save(user, updates); Type guard
function keysOverlap(a: string, b: string): boolean {
return a !== b && (a.startsWith(`${b}.`) || b.startsWith(`${a}.`));
} Try / catch
try {
await config.save(user, updates);
} catch (e) {
if (e instanceof Error && e.message.startsWith('App config paths must not overlap')) {
// drop the offending key pair and retry, or report to the admin UI
}
throw e;
} Prevention
- Store only leaf config keys; never write a parent namespace and its children in one batch
- Run the dot-boundary prefix check in the admin UI before submit
- When renaming keys during migration, delete or migrate the old row in the same change
When it happens
Trigger: Calling save(user, updates) where the updates array contains both 'flags' and 'flags.copilot' (an earlier key is a dotted ancestor of a later one), or an update key such as 'flags' that prefixes an id already stored in the app_config table. The match requires an exact dot boundary: 'flag' vs 'flags' does not overlap, 'flags' vs 'flags.x' does.
Common situations: Admin UI bulk-saving a parent namespace together with its leaf settings; migrating a flat key to a hierarchical name whose prefix matches legacy rows still present in app_config; seeding scripts that write 'a' and 'a.b' in one array.
Related errors
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/cf00bb4d69a0b39b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/models/config.ts:34
@Transactional()
async save(user: string, updates: Array<{ key: string; value: any }>) {
await this.db
.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${'app-config-paths'}, 0))`;
const existing = await this.db.appConfig.findMany({
select: { id: true },
});
const updateKeys = updates.map(update => update.key);
for (const [index, key] of updateKeys.entries()) {
const overlappingKey = [
...existing.map(config => config.id),
...updateKeys.slice(0, index),
].find(
candidate =>
candidate !== key &&
(candidate.startsWith(`${key}.`) || key.startsWith(`${candidate}.`))
);
if (overlappingKey) {
throw new Error(
`App config paths must not overlap: ${overlappingKey} and ${key}`
);
}
}
return await Promise.allSettled(
updates.map(async update => {
return this.db.appConfig.upsert({
where: { id: update.key },
update: { value: update.value, lastUpdatedBy: user },
create: { id: update.key, value: update.value, lastUpdatedBy: user },
});
})
);
}
async get(key: string) {
return await this.db.appConfig.findUnique({ where: { id: key } });View on GitHub (pinned to b4c8548c09)