HeyPuter/puter · warning · HttpError

subdomain_not_owned

subdomain_not_owned

Error message

Subdomain not owned by user

What it means

Thrown by `#ensurePuterSiteSubdomainIsOwned` when the index_url points at a puter.site subdomain whose `subdomain` row is either absent (even after a primary-store fallback check) or owned by a different user. Returns HTTP 400 with legacyCode `subdomain_not_owned`, carrying `fields.subdomain`.

Source

Thrown at src/backend/drivers/apps/AppDriver.js:1205

        }
    }

    async #ensurePuterSiteSubdomainIsOwned(indexUrl, user) {
        if (!user) return;
        const subdomain = this.#extractPuterHostedSubdomain(indexUrl);
        if (!subdomain) return;

        let row = await this.stores.subdomain.getBySubdomain(subdomain);
        if (!row) {
            // Deploys create the subdomain and immediately point the app
            // at it, so a replica or peer-cache miss here would wrongly
            // refuse the owner. Confirm against the primary before failing.
            row = await this.stores.subdomain.getBySubdomain(subdomain, {
                primary: true,
            });
        }
        if (!row || row.user_id !== user.id) {
            throw new HttpError(400, 'Subdomain not owned by user', {
                legacyCode: 'subdomain_not_owned',
                fields: { subdomain },
            });
        }
    }

    /**
     * Origin-bootstrap detection: rows auto-created when an unknown origin
     * first needed an app row (no human-supplied metadata). Marker is `name ===
     * uid && title === uid` and a description starting with "App created from
     * origin ". Only these rows are eligible for same-owner merging — refusing
     * to merge arbitrary same-owner apps prevents accidental data loss.
     */
    #isOriginBootstrapApp(app) {
        if (!app || typeof app !== 'object') return false;
        if (typeof app.uid !== 'string' || !app.uid) return false;
        if (app.name !== app.uid) return false;
        if (app.title !== app.uid) return false;

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Deploy to a subdomain you own.
  2. Register/own the target subdomain first, then point the app at it.
  3. If the deploy just created it, retry once — the primary fallback should resolve it; if not, verify the subdomain name.

Example fix

// before
object.index_url = 'https://someoneelse.puter.site/';

// after
object.index_url = `https://${mySubdomain}.puter.site/`;
Defensive patterns

Strategy: validation

Validate before calling

const sub = extractPuterSubdomain(indexUrl); // e.g. 'foo' for https://foo.puter.site
if (sub) {
  const row = await stores.subdomain.getBySubdomain(sub);
  if (!row || row.user_id !== user.id) throw new Error('subdomain not owned');
}
await driver.create({ object: { index_url } });

Try / catch

try {
  await driver.create({ object: { index_url } });
} catch (e) {
  if (e.code === 'subdomain_not_owned') { index_url = `https://${mySub}.puter.site/`; /* retry once */ return; }
  throw e;
}

Prevention

When it happens

Trigger: Creating/updating an app whose index_url uses a `<sub>.puter.site` host the acting user doesn't own. The store first checks replica/cache, then the primary store, to avoid false negatives during deploys.

Common situations: Pointing an app at another user's deployed puter.site subdomain; a stale cache miss on a freshly created subdomain during a deploy; typo in the subdomain name.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/a29da6398e871828. Report an issue: GitHub.