Skip to content

First-run setup is provisioning code running inside the serving app, gated on a test flag #2260

Description

@KonstantinMirin

The rule being proposed

A production deployment should contain no provisioning code. Creating the first tenant,
minting the first admin credential and writing sample rows are setup, not serving —
a distinct mode, entered on an explicit signal, and closed by a state change that cannot
be re-entered from the web once the install is configured.

Today all three run inside the serving application, on every start, and one of them asks
whether a test suite is running.

What runs at boot today

init_db() is called on every production start — scripts/deploy/run_all_services.py:181,
and src/core/main.py:211. The comment immediately above the call reads:

ℹ️ Note: init_db() is safe - it only creates tables (IF NOT EXISTS) and default tenant (if no tenants exist)

That sentence is the assumption worth revisiting: creating a tenant is not a safe no-op,
it is provisioning.

What Where Gated by
tenant_id="default", subdomain="default" src/core/database/database.py:45-72 nothing. CREATE_DEMO_TENANT picks the SHAPE (configured demo vs "My Sales Agent" awaiting setup); both branches create the row
mock adapter, currency limits for USD/EUR/GBP, and a principal holding the literal token ci-test-token database.py:90-110 CREATE_DEMO_TENANT=true
sample products and inventory database.py:142, :231 provisioning.create_sample_data (config.py:186)
the admin bootstrap login route src/admin/app.py:350-351 settings.testing.adcp_auth_test_mode and not is_production

The last row is the one that makes the shape visible: production composition asks whether
a suite is running.
The app under test is therefore not the app deployed, and a test that
wants the route has to set a test flag to conjure it. That is a first-run bootstrap
capability named, credentialled and gated as a test fixture.

Consequences already filed

  • Tenant resolution falls back to the 'default' tenant for a loopback Host; every deployment has one #2259 — tenant resolution falls back to the default tenant for a loopback Host.
    That fallback is only reachable because default always exists; a deployment that was
    never provisioned with one would have nothing to fall into.
  • Seven production properties branch on ADCP_TESTING; the ban is in, the forks remain #2255 — production properties branching on ADCP_TESTING. Same class one frame down:
    an allowance for the machine running the code, surviving into the path that serves buyers.
  • Two integration tests (test_template_url_validation.py::test_all_template_url_for_calls_resolve
    and ::test_form_actions_point_to_valid_endpoints) pass or fail depending on whether the
    test flag is on
    , because they scan templates for url_for() with no awareness of the
    {% if test_mode %} guard around the bootstrap route. They are ledgered pending this
    decision, and they are the symptom that surfaced it.

This is a solved problem; the pattern is worth copying rather than inventing

Every CMS and framework has settled on the same shape, and the shared property is not the
UI — it is that setup is closed by durable state, not by a flag:

  • WordPress: wp-admin/install.php runs the wizard, and refuses once the options table
    carries a siteurl. The route stays on disk and becomes permanently inert.
  • Django: migrate and createsuperuser are explicit management commands; the serving
    app has no route that can create a superuser.
  • Drupal: an install profile runs once, and settings.php gaining a hash closes it.
  • Rails: db:setup / db:seed are tasks, never request handlers.

So the design question is which signal opens setup and which state closes it — not whether
a CLI or a wizard. A wizard is friendlier than a CLI, as noted, and a wizard is perfectly
compatible with "no provisioning code in production" provided the gate is a database fact
(no configured tenant / no admin identity) rather than an environment variable, and
provided re-entry after that fact changes requires wiping the database.

Where this sits relative to the AuthZ work

Adjacent, not overlapping, and the seam between them is worth stating:

They meet at exactly one point: whatever mints the first admin credential produces an
identity that #2232's authenticators and #2256's one admin seat then handle like any other.
Setup should hand off to that vocabulary and own nothing beyond it.

Scope for a first pass

  1. Decide the gate: which database fact means "not yet set up", and what closes it.
  2. Move tenant creation, credential minting and sample data out of the serving app.
  3. Delete adcp_auth_test_mode from composition, so the deployed app and the tested app
    are the same app.
  4. Graduate the two ledgered test_template_url_validation entries, whose verdict stops
    depending on a flag.
  5. Re-examine Tenant resolution falls back to the 'default' tenant for a loopback Host; every deployment has one #2259's fallback, which may simply disappear with the unconditional default
    tenant that makes it reachable.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions