# Lifecycle and config

> Attach external catalogs, rotate secrets, back up and restore configuration, and decommission a DuckDB gateway cleanly, step by step.

Attach external catalogs, rotate secrets, back up and restore configuration, and decommission cleanly. Each playbook below follows the same structure: goal, prerequisites, step-by-step UI flow with screenshots, the equivalent [qod CLI](/qod/cli/) command, and how to verify success.

## Attach an external catalog

**Goal**: Register an external data source (Postgres, S3, Iceberg, or anything a DuckDB extension can `ATTACH`) as a federated catalog on a tenant database. Clients can then query remote data through the same FlightSQL session and ACL model as native DuckLake tables.

**Prerequisites**:

- The tenant and database already exist. See [Tenants and databases](/qod/operating/tenants-databases).
- The target external system is reachable from the Quack nodes at startup time.
- `qod login` has stored a session (CI scripts can use `QOD_API_KEY` instead). The manager runs at `http://localhost:20900` (the default dev address; adjust the host and port for other environments).

**Steps (UI)**:

1. Pick the target tenant in the sidebar's tenant switcher.
2. Open **Tenant > Databases** in the sidebar and find the target database.
3. Click **Federation** to open the federated-source panel.

   ![Federation panel](/img/ui/federation.png)

4. Click **Add source**. Enter the alias (e.g. `fedpg`), an optional description, and the `setupSql`. Use `{{alias}}` in the `ATTACH` clause as the catalog name, and `{{secret.NAME}}` wherever a credential value should appear. The manager substitutes both placeholders before the SQL reaches a node.
5. Click **Add secret** and enter the secret name (e.g. `PG_PWD`) and its inline value (or an `externalRef` for an externally-managed secret).
6. Save. The source takes effect the next time a node starts in this pool.

**Manifest (YAML)**

```yaml
tenants:
  - name: acme
    tenantDbs:
      - name: acme_fed
        federatedSources:
          - alias: fedpg
            description: Prod warehouse Postgres
            setupSql: |
              INSTALL postgres; LOAD postgres;
              CREATE OR REPLACE SECRET fedpg_sec (TYPE POSTGRES, HOST 'pg.prod', PORT 5432, DATABASE 'warehouse', USER 'svc_qod', PASSWORD '{{secret.PG_PWD}}');
              ATTACH '' AS {{alias}} (TYPE POSTGRES, SECRET fedpg_sec, READ_ONLY);
            secrets:
              - { name: PG_PWD, value: hunter2 }
```

**CLI equivalent**:

```bash
# Register the federated source
qod federation create acme acme_fed --alias fedpg --description "Prod warehouse Postgres" \
  --setup-sql "INSTALL postgres; LOAD postgres; CREATE OR REPLACE SECRET fedpg_sec (TYPE POSTGRES, HOST 'pg.prod', PORT 5432, DATABASE 'warehouse', USER 'svc_qod', PASSWORD '{{secret.PG_PWD}}'); ATTACH '' AS {{alias}} (TYPE POSTGRES, SECRET fedpg_sec, READ_ONLY);"

# Add the secret referenced by {{secret.PG_PWD}}
qod federation secret set acme acme_fed fedpg --name PG_PWD --value hunter2
```

Placeholders in `setupSql`:
- `{{alias}}` is replaced with the source's `alias` field.
- `{{secret.NAME}}` is replaced with the resolved value of the secret named `NAME`.

**Verify**: Run a query that references the alias (e.g. `SELECT * FROM fedpg.public.orders LIMIT 5`). Open the catalog browser to confirm the alias and its schemas appear.

![Catalog browser](/img/ui/catalog.png)

**Related**: [Federation](/qod/operating/federation), [Catalogs](/qod/concepts/catalogs).

---

## Rotate a federated secret

**Goal**: Replace a stored credential (e.g. a rotated database password) without touching the `setupSql`, then apply the new value to running nodes.

**Prerequisites**:

- The federated source exists and has at least one value-backed secret.
- You understand the lifecycle rule: secret changes take effect on the next node spawn only. Already-running nodes keep the credential they received at startup until they exit. Recycle the pool to apply the change immediately.

**Steps (UI)**:

1. Pick the tenant in the sidebar's tenant switcher, open **Tenant > Databases**, and click **Federation** on the relevant database.
2. Find the federated source and open its secret list.
3. Click the secret row and enter the new value. The `PUT` is an upsert; only the value field changes, not the `setupSql`.
4. Open **Tenant > Pools** and click the name of the pool that uses this database to open its detail page.
5. In the header, choose **Suspend** -> **Drain** to stop accepting new queries, then wait for in-flight statements to complete. Click **Scale** to bring nodes back up (or use **Suspend** -> **Kill** if immediate replacement is acceptable). New nodes start with the updated secret value injected into their `setupSql`.

**Manifest (YAML)**

Re-import the full source with every secret you want to keep: omitted secrets under a listed source are pruned.

```yaml
tenants:
  - name: acme
    tenantDbs:
      - name: acme_fed
        federatedSources:
          - alias: fedpg
            setupSql: |
              INSTALL postgres; LOAD postgres;
              CREATE OR REPLACE SECRET fedpg_sec (TYPE POSTGRES, HOST 'pg.prod', PORT 5432, DATABASE 'warehouse', USER 'svc_qod', PASSWORD '{{secret.PG_PWD}}');
              ATTACH '' AS {{alias}} (TYPE POSTGRES, SECRET fedpg_sec, READ_ONLY);
            secrets:
              - { name: PG_PWD, value: <new-value> }
```

**CLI equivalent**:

```bash
# Re-set the secret with the new value (same command as the initial add; it upserts)
qod federation secret set acme acme_fed fedpg --name PG_PWD --value hunter2
```

After updating the secret, recycle the pool so new nodes pick up the change. See [Federation - lifecycle](/qod/operating/federation#lifecycle) for the exact spawn-time substitution rules.

**Verify**: After the pool recycles, run a federated query. It should succeed with the new credential. A failure at startup (`unresolved secret` or a DuckDB connect error in the supervisor log) means the new value is wrong or the secret name is mismatched.

**Related**: [Federation](/qod/operating/federation).

---

## Back up and restore

**Goal**: Export the entire control-plane configuration to a YAML manifest for backup, version control, or environment cloning, then re-import an edited file.

Note: the Config page (with its Manifest section) and the manifest endpoints are restricted to superusers. A tenant-scoped session receives `403 superuser_required`.

**Prerequisites**:

- Signed in as a superuser (the **Settings > Config** entry in the sidebar is only visible to superusers).

**Steps (UI)**:

1. Open **Settings > Config** in the sidebar (`/ui/settings/config`).
2. Select the **Manifest** section.
3. Click **Download YAML** to export the current configuration. The file contains all tenants, databases, pools, federated sources, roles, groups, and users. Secret values are written as `***REDACTED***`; plaintext user passwords are never written, but each user's bcrypt `passwordHash` is exported verbatim so a restore keeps the same credential. Treat the downloaded file as sensitive and do not commit it to a public repository.
4. Edit the file as needed. Leave `***REDACTED***` in place for secrets you do not intend to change; the importer reuses the existing stored value for those entries.
5. Click **Import** and upload the edited YAML.

![Config page](/img/ui/config.png)

**CLI equivalent**:

```bash
# Export - superuser only
qod manifest export --out manifest.yaml

# Import after editing
qod manifest import manifest.yaml
```

The import response is a JSON count: `{"tenants":2,"tenantDbs":3,"pools":4,...}` (pass `--json` to see it verbatim). The import validates the whole document before writing anything; on failure it returns `400` and changes nothing.

**Verify**: After import, check the tenant list (**Manage tenants...** in the tenant switcher) and the **Users** page under **All tenants** to confirm the expected tenants, pools, and users are present. If you cloned to a new environment, run a sample query through the FlightSQL edge to confirm the pools came up correctly.

**Related**: [Manifest backup and restore](/qod/operating/manifest).

---

## Decommission

**Goal**: Cleanly remove a pool, a tenant, or individual RBAC objects (users, roles, groups) from the control plane, freeing resources and preventing stale access.

**Prerequisites**:

- To delete a tenant, all of its pools must already be deleted. Attempting to delete a tenant that still has pools returns an error.
- Deleting a pool with `force: true` stops nodes immediately; outstanding queries fail. Use **Drain** first if you need a graceful stop.

**Steps (UI)**:

1. **Delete a pool**: Pick the tenant in the sidebar's tenant switcher, open **Tenant > Pools**, click the pool name to open its detail page, and click the **Delete** (trash) button in the header. This stops all nodes and removes the pool from the registry. To stop nodes temporarily without removing the pool, use **Suspend** -> **Drain** instead.
2. **Delete a tenant**: Once all pools are removed, choose **Manage tenants...** in the tenant switcher and click the delete action on the tenant's row.
3. **Remove a user, role, or group**: Pick the tenant in the tenant switcher, open **Users**, **Groups**, or **Roles** under **Users & Access Controls**, find the object, and click **Delete**.

**Manifest (YAML):** Declaratively, drop a pool from a tenant that is still in the manifest and re-import; nested collections are replaced, so the omitted pool is pruned. Top-level tenants are upsert-only, so deleting a whole tenant uses the CLI delete below, not omission. See [Manage by manifest](/qod/administration/manage-by-manifest).

**CLI equivalent**:

```bash
# Delete a pool: stops nodes AND removes the pool from the registry
qod pool delete --tenant acme --db acme_tpch --pool bi --force

# Delete a tenant (must have no pools first)
qod tenant delete acme
```

**Verify**: The pool or tenant no longer appears in the tenant list or on the **Dashboard**. A user removed from the RBAC graph can no longer authenticate against the tenant or run queries.

**Related**: [Tenants and databases](/qod/operating/tenants-databases), [Administering access](/qod/operating/rbac-admin).
