The production API image contains two operator entrypoints: database migration and one-time administrator bootstrap. There is no general customer message-operation CLI; use the UI or [REST API](https://portal.cjcit.io/knowledge-base/message_ops/1.7/reference/api/).

Run these as separate, explicitly started Azure Container Apps jobs. API/Worker images have no shell or package manager. The executable is `/nodejs/bin/node`; do not override the command with `sh`, `pnpm` or a PATH-dependent `node`.

## Database migration

Use the approved 1.7.1 API image and the **migration identity/database secret**. The reference job runs from `/app` with:

```
command: [/nodejs/bin/node]
args:
  - --import
  - file:///app/apps/api/dist/observability/otel-preload.js
  - apps/api/dist/cli/migrate.js
```

Supply `NODE_ENV=production` and `DATABASE_URL` (or `DATABASE_URL_FILE`), with applicable database CA trust. The migrator does not require the API’s runtime encryption keys. It applies ordered application migrations, installs/upgrades the PostgreSQL job queue, creates configured queues and grants the restricted runtime role access. It exits 0 on success, including an already-current database, or 1 on failure. It retries an initial unavailable connection 30 times with one-second delays.

Set `RESOURCE_GROUP` and `MIGRATION_JOB` to your resource names:

```
EXECUTION=$(az containerapp job start -g "$RESOURCE_GROUP" -n "$MIGRATION_JOB" --query name -o tsv)
az containerapp job execution show -g "$RESOURCE_GROUP" -n "$MIGRATION_JOB" \
  --job-execution-name "$EXECUTION" --query properties.status -o tsv
```

Repeat the status query for **that execution** until it is terminal. Continue only for `Succeeded`, with `Database migrations are up to date.` in its logs and no migration errors. Use the named execution’s Portal/Log Analytics logs; completed replicas may no longer provide live streaming.

Verify the migration ledger matches the release, `platform_settings` and `observability_destinations` exist, and the job-queue schema/queues are installed. A SQL-only partial success is insufficient. If a later queue/grant stage fails, correct the cause and rerun the same release. Unknown migration versions indicate a release/database mismatch; never edit the ledger or drop production data to bypass it.

## Runtime database login

The first migration creates `message_ops_app` as a restricted **NOLOGIN** role. Enable it only after migration succeeds. Using a protected PostgreSQL service/passfile or interactive prompt from an approved private-network host, connect as the migration owner:

```
PSQL_HISTORY=/dev/null psql 'service=message-ops-migration'
```

The named PostgreSQL service must already contain your host/database/user/TLS configuration; it is not installed by Message Ops. Then run:

```
ALTER ROLE message_ops_app LOGIN;
\password message_ops_app
GRANT CONNECT ON DATABASE message_ops TO message_ops_app;
```

Choose an independently generated password at the private prompt. Do not grant ownership, CREATE, CREATEROLE, CREATEDB or superuser rights. Build a percent-encoded TLS URL in a protected file. This operator-host example needs Python 3:

```
umask 077
python3 -c '
import getpass, os, warnings
from urllib.parse import quote
warnings.simplefilter("error", getpass.GetPassWarning)
host = input("PostgreSQL FQDN: ").strip()
if not host:
    raise SystemExit("PostgreSQL FQDN is required.")
password = getpass.getpass("Runtime database password: ")
encoded_password = quote(password, safe="")
url = f"postgresql://message_ops_app:{encoded_password}@{host}:5432/message_ops?sslmode=verify-full"
fd = os.open("runtime-db-url.txt", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as output:
    output.write(url)
' &&
az keyvault secret set --vault-name "$VAULT" --name "$RUNTIME_DB_SECRET" \
  --file runtime-db-url.txt --output none &&
rm -f runtime-db-url.txt
```

Set `VAULT` and `RUNTIME_DB_SECRET` to the intended vault and runtime-secret names before running the block in an interactive terminal. Password input is hidden; the example refuses an insecure non-terminal fallback. The upload runs only after successful file creation, and cleanup runs only after a successful upload. An existing output file is never overwritten. If generation or upload fails, stop, investigate and handle any retained file under your approved temporary-workspace cleanup policy. Keep the migration URL distinct. Connect as the runtime role and check:

```
SELECT current_user;
SELECT has_schema_privilege(current_user, 'public', 'CREATE') AS public_ddl,
       has_schema_privilege(current_user, 'pgboss', 'CREATE') AS queue_ddl,
       has_function_privilege(current_user, 'purge_audit_events(timestamptz)', 'EXECUTE') AS audit_purge;
```

Expect `message_ops_app`, then `false`, `false`, `true`. Check reads of platform settings, role flags in `pg_roles`, and a completed queued test operation after activation. The migration owner remains responsible for future object/default privileges.

## Administrator bootstrap

After migration and runtime credential verification, run `apps/api/dist/cli/bootstrap-admin.js` once with the **runtime** database credential. The CLI does not start the API server or create a browser session.

<table><caption class="sr-only">Administrator bootstrap</caption><thead><tr><th scope="col">Argument</th><th scope="col">Requirement</th></tr></thead><tbody><tr><td><code>--email</code></td><td>Founding administrator’s sign-in email.</td></tr><tr><td><code>--display-name</code></td><td>Administrator display name.</td></tr><tr><td><code>--password-file</code></td><td>Required for a non-interactive job; readable mounted secret file.</td></tr><tr><td><code>--help</code> or <code>-h</code></td><td>Show usage.</td></tr></tbody></table>

`--password` is rejected. Without a file, only a real interactive TTY may enter and confirm a password; piped stdin is not accepted. Password files preserve spaces and remove exactly one trailing LF or CRLF. Passwords must be 15–200 characters and pass the common/default/identity-derived password checks.

Create a temporary vault secret from a protected password file using `az keyvault secret set --file ... --output none`. Grant the application identity read access to that temporary secret and make the mount readable by UID 65532. Save this template as `bootstrap-job.yaml`, replacing every placeholder with your resource names, URIs, identity and approved digest. No placeholder should contain a raw secret:

```
location: <azure-region>
identity:
  type: UserAssigned
  userAssignedIdentities:
    <app-identity-resource-id>: {}
properties:
  environmentId: <container-app-environment-resource-id>
  configuration:
    triggerType: Manual
    replicaTimeout: 300
    replicaRetryLimit: 0
    manualTriggerConfig:
      parallelism: 1
      replicaCompletionCount: 1
    registries:
      - server: <acr>.azurecr.io
        identity: <app-identity-resource-id>
    secrets:
      - name: runtime-db-url
        keyVaultUrl: https://<vault>.vault.azure.net/secrets/<runtime-db-secret>
        identity: <app-identity-resource-id>
      - name: admin-password
        keyVaultUrl: https://<vault>.vault.azure.net/secrets/<temporary-bootstrap-secret>
        identity: <app-identity-resource-id>
  template:
    containers:
      - name: bootstrap
        image: <acr>.azurecr.io/message-ops/api@sha256:<64-hex-digest>
        command: [/nodejs/bin/node]
        args:
          - apps/api/dist/cli/bootstrap-admin.js
          - --email
          - <administrator-email>
          - --display-name
          - <administrator-display-name>
          - --password-file
          - /mnt/bootstrap/admin-password
        env:
          - name: DATABASE_URL
            secretRef: runtime-db-url
          - name: NODE_ENV
            value: production
        resources:
          cpu: 0.5
          memory: 1Gi
        volumeMounts:
          - volumeName: bootstrap-password
            mountPath: /mnt/bootstrap
    volumes:
      - name: bootstrap-password
        storageType: Secret
        secrets:
          - secretRef: admin-password
            path: admin-password
```

Set `BOOTSTRAP_JOB` to a temporary job name and `RESOURCE_GROUP` to its resource group:

```
az containerapp job create -g "$RESOURCE_GROUP" -n "$BOOTSTRAP_JOB" --yaml bootstrap-job.yaml
BOOTSTRAP_EXECUTION=$(az containerapp job start -g "$RESOURCE_GROUP" -n "$BOOTSTRAP_JOB" --query name -o tsv)
az containerapp job execution show -g "$RESOURCE_GROUP" -n "$BOOTSTRAP_JOB" \
  --job-execution-name "$BOOTSTRAP_EXECUTION" --query properties.status -o tsv
```

Add the database CA setting/mount to this job if the database uses a private CA. Follow the named execution to `Succeeded` and confirm `Initial administrator provisioned successfully.` in its logs. Sign in normally and complete mandatory MFA, then license activation and [Application Setup](https://portal.cjcit.io/knowledge-base/message_ops/1.7/getting-started/application-setup/).

<table><caption class="sr-only">Administrator bootstrap</caption><thead><tr><th scope="col">Exit code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Administrator provisioned.</td></tr><tr><td>1</td><td>Internal or database connection failure.</td></tr><tr><td>2</td><td>Invalid arguments, missing input or password mismatch.</td></tr><tr><td>3</td><td>Installation already initialized.</td></tr><tr><td>4</td><td>An administrator already exists.</td></tr><tr><td>5</td><td>Password policy violation.</td></tr></tbody></table>

After successful sign-in, remove the temporary job, password file, temporary secret access grant and secret under your retention policy. Preserve redacted execution evidence. Bootstrap is not a password reset or recovery command: it permanently marks initialization. Maintain more than one accountable administrator and saved MFA recovery methods. For lockout, use the controlled support process in [Troubleshooting](https://portal.cjcit.io/knowledge-base/message_ops/1.7/troubleshooting/).
