toeverything/AFFiNE · error · Error

App config paths must not overlap: ${overlappingKey} and ${k

Error message

App config paths must not overlap: ${overlappingKey} and ${key}

What it means

Plain Error thrown by AppConfigModel (packages/backend/server/src/models/config.ts:34) during a bulk app-config update. For each key in updates it scans existing config ids plus previously-seen update keys for a hierarchical overlap: one is a prefix of the other via startsWith('x.'). The guard prevents two configs from shadowing each other in a dotted namespace (e.g. 'a' vs 'a.b').

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 26c515e050)

Solutions

  1. Split the batch so ancestor and descendant are never updated together; update leaves first, then parents.
  2. Delete the colliding existing key (db.appConfig.delete) before inserting its ancestor/descendant.
  3. Flatten your config schema so keys do not share prefixes, or adopt a non-dotted separator.
  4. Inspect existing ids first: const existing = await db.appConfig.findMany({ select: { id: true } }); and diff against your proposed keys.

Example fix

// before
await configModel.setMany(user, [
  { key: 'feature', value: 'on' },
  { key: 'feature.flag', value: 'off' }, // overlap -> throws
]);
// after
await configModel.setMany(user, [{ key: 'feature.flag', value: 'off' }]);
await db.appConfig.deleteMany({ where: { id: { startsWith: 'feature.' } } });
await configModel.setMany(user, [{ key: 'feature', value: 'on' }]);
Defensive patterns

Strategy: validation

Validate before calling

function findOverlap(keys: string[], existing: string[]): [string, string] | null {
  for (const key of keys) {
    const hit = [...existing, ...keys.filter(k => k !== key)].find(c =>
      c !== key && (c.startsWith(`${key}.`) || key.startsWith(`${c}.`))
    );
    if (hit) return [hit, key];
  }
  return null;
}
const existingIds = (await db.appConfig.findMany({ select: { id: true } })).map(r => r.id);
if (findOverlap(updates.map(u => u.key), existingIds)) throw new Error('overlap');

Prevention

When it happens

Trigger: Posting an app-config update batch where one key is an ancestor/descendant of another. Triggers when candidate.startsWith(`${key}.`) OR key.startsWith(`${candidate}.`) for any earlier update key or any existing appConfig.id. Example: updating [{key:'feature.x'}, {key:'feature'}] in one call, or adding 'feature' when 'feature.x' already exists.

Common situations: Migrating a flat key to a namespace (or vice versa) without first deleting children; bulk-importing configs with mixed nesting depths; UI saving both a parent toggle and a child setting in the same payload; renaming a config namespace.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/cf00bb4d69a0b39b. Report an issue: GitHub.