This is the single operational guide for removing Supabase authentication from Nexent. Run commands from the repository root. Docker and Kubernetes use the application PostgreSQL database for accounts; ordinary deployments do not start Supabase, GoTrue or Kong. The legacy YAML files beside this guide are recovery assets for the original source database.
Migration completion depends on account data, not table existence. Every active historical member must have usable local credentials, a recognized API-only, provider or SPEED identity, an explicitly disabled local account, or per-user source-unavailability exclusion evidence. New imports must pass this validation before commit. Existing incomplete tables stop deployment and require consistent backup recovery; no automatic repair runs. No historical membership relation means fresh initialization. Versions and SQL checksums do not select migration.
Run the importer before account SQL or the new application. If account SQL already removed legacy role/email fields and no account holds their canonical copies, the importer stops with unresolved IDs and performs no automatic repair. Restore those business profiles from the target's pre-upgrade backup; Supabase cannot reconstruct application roles.
| Stage | Docker | Kubernetes |
|---|---|---|
| Database ready | Start selected infrastructure; PostgreSQL initializes only deploy/sql/init.sql on an empty data directory |
Install/wait for infrastructure; its SQL ConfigMap contains only init.sql |
| Historical account import | Host Shell migrate.sh --apply runs before HTTPS/core service startup |
Host Shell migrate.sh --deployment kubernetes --apply runs before application Secret replacement and application Helm rollout |
| Target transaction | Recheck account coverage under the account-writer lock, run historical 001 and account-array 010, transfer profiles and credentials, then commit | Same transaction through target PostgreSQL pod |
| Normal application SQL | Shared Workbench initialization and Config startup may run the normal SQL runner after import | Application Helm rollout runs SQL in Config or its bootstrap Job after import, then waits for business workloads |
| Administrator bootstrap | Run only after core deployment and its SQL succeed | Run only after the application bootstrap/rollout succeeds |
The infrastructure baseline init.sql does not create user_account_t. Account
SQL is kept out of infrastructure bootstrap. A migration error stops deployment
before the subsequent application/Secret rollout. Direct Compose, Helm or
run-sql-migrations.sh invocation bypasses this deployment sequence; complete the
manual importer first when upgrading historical accounts.
The importer keeps source rows in memory. Target schema creation, membership
email/role transfer and credential import share one transaction. Failure or
preview rolls back the new account table, its sequence and temporary import data,
and restores the original membership columns/data. Retry reads the source again.
There are no persistent credential snapshots or phase files. Intentionally
unavailable source identities get ID/reason/audit-only records in
nexent.account_migration_exclusion_t in the same transaction. These records let
later deployments distinguish intentional exclusions after source removal.
An existing incomplete table is not imported into or modified automatically.
For a new migration, failed validation rolls back the new table, exclusions and
profile moves before commit. Completed local accounts are preserved on rerun.
The immutable v2.9.0_001_local_user_accounts.sql creates the legacy account schema, without
account_status or pending placeholders. Legacy status columns are converted to
delete_flag and dropped inside the same transaction; ordinary replay never
recreates them. Existing soft-deleted accounts, credentials and operator changes
are preserved on replay. The former 002 is consolidated into 001; existing SQL
execution history is retained. Transaction-local membership profiles supply roles
and emails to the importer without persisting unavailable ordinary identities.
Active CAS/OAuth bindings remain in the project database; provider-only accounts
have no local password login, while actual local password credentials retain it.
The final account schema stores only user_roles; scalar response roles are
computed. The importer applies 010 after historical 001 inside the same transaction
and writes only the array. Inspection recognizes both layouts without recreating
columns. Historical replay is followed by 010, which preserves existing arrays
and drops any restored scalar column. Operations grants remain in independent
011; account recovery has no dependency on operations tables.
Back up the original Supabase and target PostgreSQL data, settings and signing keys. Freeze registration/password/provider changes during cutover. Use a matching checkout, deployment package and backend image. Retain original storage until acceptance completes.
The host needs Bash and Docker CLI, or Bash and kubectl with access to the original
namespace. No host Python, pip, PostgreSQL driver or jq is required. The selected
NEXENT_IMAGE must package the current Python worker, its existing psycopg2
runtime, v2.9.0_001_local_user_accounts.sql, and
v2.9.0_010_account_and_ops_roles.sql. The worker applies the account schema changes
inside the credential transaction. The startup SQL runner also applies invitation
roles and operations grants. Standard deployment selects the image automatically.
Preload it for offline deployment. The migration worker uses the main backend image.
migrate.sh is the host entry point for both Docker and Kubernetes.
migrate_docker.py runs inside the worker container or Job. Recovery defaults
for images, ports and service names are provided by legacy.sh.
Existing deployment settings are reused. Exported values override the selected
Shell-compatible --env-file (default deploy/env/.env). The source password
prefers SUPABASE_POSTGRES_PASSWORD, then the original container/pod environment;
manual interactive execution prompts only when both are unavailable.
Noninteractive missing credentials stop the upgrade. Changing an environment
password does not change an existing PostgreSQL cluster's password.
| Setting | Purpose |
|---|---|
NEXENT_IMAGE |
Version-matched backend image; set explicitly for standalone commands |
NEXENT_POSTGRES_PASSWORD, POSTGRES_USER, POSTGRES_DB |
Original target credentials |
SUPABASE_POSTGRES_PASSWORD, SUPABASE_POSTGRES_USER, SUPABASE_POSTGRES_DB |
Original source credentials |
SUPABASE_POSTGRES_HOST, SUPABASE_POSTGRES_PORT |
Explicit remote source endpoint, reachable from the worker |
SUPABASE_MIGRATION_DB_CONTAINER |
Explicit original Docker source when discovery is ambiguous |
SUPABASE_MIGRATION_AUTH_CONTAINER, SUPABASE_MIGRATION_KONG_CONTAINER |
Exact Docker peers reclaimed after successful apply |
Do not add migration DSN or MIGRATION_SOURCE_* / MIGRATION_TARGET_* variables.
bash deploy.sh docker
# Or:
bash deploy.sh k8sThe shared Shell hook runs before application SQL, application Secret replacement and business rollout. It executes psql inside the target DB container/pod to classify the target. An existing account table skips import; absence of both account/membership tables means fresh installation; membership without accounts requires historical import. Missing original sources stop legacy upgrades.
Docker uses an owned one-shot container with the backend image's entrypoint
overridden to the Python worker. It attaches that container to the existing target
and source bridge networks, uses actual database internal ports/IPs and requires
no published DB ports. A source on the host can use its published port; loopback
addresses are translated to the worker network's host gateway. Host-network-only
DB containers need a reachable explicit endpoint/bridge network. The worker has
no Docker socket. Docker starts an existing stopped source. If the original DB
container was removed, the migration recreates just the DB service using the
recovery Compose YAML and SUPABASE_DATA_DIR or ROOT_DIR/volumes/db/data.
Both the host and the container check for PG_VERSION and global/pg_control
before PostgreSQL starts. Use the original database password. An applied,
committed migration removes the recovered container and retains its data;
failure or preview retains the container for retry.
Kubernetes uses a separate Job before application Helm startup. The host resolves
original database endpoints/credentials and preserves source Deployment/PVC/PV
recovery definitions beside the selected env file under backups/, then sets
bound PV reclaim policies to Retain. Missing, ephemeral or unbound source storage
blocks the upgrade. A zero-replica source is scaled up and awaited. The Job receives
credentials through an owned temporary Secret, has no mounted Kubernetes API
token, uses no control CLI, and bypasses application startup SQL. It inherits
existing Config image-pull Secret references, or the namespace's default ServiceAccount
pull configuration. Configure registry access before migrating if those references
are unavailable. Network policies must allow source/target PostgreSQL access.
Python connects to both DBs inside the worker, reads a consistent read-only source snapshot, rechecks completion under the writer lock, and applies the existing schema/credential transaction. It never invokes Docker/kubectl. The host removes its own worker container or Job/Secret on exit. Temporary host Secret input files are private and removed after Job creation or on failure. No account snapshots are written to disk. Job timeouts and migration failures stop subsequent rollout while retaining source DB/Auth/Kong workloads. Only a committed successful apply reclaims those source workloads, with persistent storage/backups retained. Cleanup errors after commit are reported separately and do not erase imported credentials.
Keep the original namespace, context, Helm values, Secrets and ConfigMaps for recovery. This operation does not convert legacy single-release Helm ownership into the current two-release layout; resolve ownership errors first.
Run before application/bootstrap SQL against the original business PostgreSQL. The host uses the same Shell entry for both deployment types:
export NEXENT_IMAGE='nexent/nexent:<matching-version>'
# Docker preview, then apply:
bash deploy/sql/tools/supabase_to_postgres/migrate.sh
bash deploy/sql/tools/supabase_to_postgres/migrate.sh --apply
# Kubernetes preview, then apply:
bash deploy/sql/tools/supabase_to_postgres/migrate.sh --deployment kubernetes --namespace nexent
bash deploy/sql/tools/supabase_to_postgres/migrate.sh --deployment kubernetes --namespace nexent --apply--database-target NAME selects the target DB container or Kubernetes workload.
--source-container NAME selects the original source; --no-start-source rejects
stopped sources and disables automatic Docker recovery. --wait-timeout accepts 1–300 seconds (default 300). Kubernetes
--config-target selects the existing Config Deployment used only for registry
pull references; no running Config is required. Preview rolls back all target
schema/data changes and retains source workloads/storage.
After success, run normal deployment. The regular SQL runner records/replays consolidated 001 without recreating the retired status column.
Recovery YAML, production overlay, Kong configuration and legacy.env.example
remain beside this guide. Use original image versions, passwords and data files.
The host Shell refuses recovery unless the data directory contains PG_VERSION
and global/pg_control:
export ROOT_DIR=/absolute/path/to/original/nexent-data
# Or export SUPABASE_DATA_DIR directly.
bash deploy/sql/tools/supabase_to_postgres/migrate.sh up
# Optional complete old stack/production overlay, using original auth keys:
bash deploy/sql/tools/supabase_to_postgres/migrate.sh up --with-auth --production
# Exact configured source containers only; retain volumes and data:
bash deploy/sql/tools/supabase_to_postgres/migrate.sh downDB-only recovery is the default. Full auth needs original JWT_SECRET,
SUPABASE_KEY, SERVICE_ROLE_KEY and DASHBOARD_PASSWORD. Existing stopped
containers start intact. A partially present requested stack requires explicit
down/up against verified original data. Compose uses create_host_path: false
and never restores a backup automatically. Avoid down -v, volume pruning and
source-directory deletion. No recovery or migration command needs host Python.
| Identity/data | Migration behavior |
|---|---|
| Supabase password users | Preserve UUID, compatible bcrypt/scrypt hash, source timestamps, email confirmation and bounded provider metadata |
| Banned/deleted/anonymous users and orphans | Exclude from the new account table. Original Supabase records and membership rows remain; no role or tenant is synthesized |
| API-only batch users | Active blank-email USER/DEV membership plus historical token evidence creates a passwordless account; preserve ID, original keys and revoked key state |
| Default SPEED seed | Preserve user_id / tenant_id, mark system_identity=speed, keep unavailable for full-mode login. The retired SPEED role is removed without granting replacement permissions |
| Unknown/custom identities | Unresolved active members stop migration with IDs; recover source/business profiles without generating replacement IDs or roles |
| CAS/OAuth | Existing provider bindings and CAS sessions remain in business PostgreSQL. Preserve original IDs and valid bindings. Provider-only identities stay passwordless; accounts with actual local password credentials retain password login. Import Supabase credentials only where those source identities exist |
| Audit and refresh data | Do not read/import auth.audit_log_entries, Supabase sessions, refresh tokens or recovery/confirmation secrets. No audit archive is created |
Canonical user_email and the legacy role move from user_tenant_t to the account.
The role is stored only in user_roles; scalar API values are derived from that array;
tenant, group, key and business ownership remain unchanged. Provider metadata
never supplies an application role. source counts rows read from the original source.
updated counts hydrated legacy profiles,
inserted counts source identities absent from those profiles, and
disabled_orphans counts excluded source users without active tenant membership; excluded_inactive counts all excluded source identities. Repeat
imports inspect coverage and preserve newer credentials/edits.
Ordinary login uses signed JWTs without a server session table. The existing
session response envelope stays compatible, with an empty refresh token.
Expired tokens require login; the legacy refresh endpoint rejects old credentials.
Logout clears cookies; a copied ordinary JWT remains valid until expiry unless
its credential revision or account availability changes. Password changes write
salted scrypt and increment the credential revision. CAS retains its dedicated
session table and sid revocation. Registration still requires an invitation.
Keep JWT_SECRET stable across application services and repeated deployments.
The new issuer and required credential revision reject legacy Supabase JWTs.
The normal SU bootstrap uses suadmin@nexent.com, the configured
NEXENT_SUPER_ADMIN_PASSWORD or the existing default. It prints credentials only
after confirming a newly created account; existing/error/manual-hidden flows do
not print them. Re-running deployment cannot recover a password from its hash.
Email reuse is checked after final account eligibility is computed. Soft-deleted, banned, anonymous or orphaned ordinary identities do not occupy an email in the new account table. Temporary schema-created unavailable profiles are removed in the same transaction, except the recognized SPEED system identity. Only final active duplicate emails fail; logs contain the conflicting account IDs without email values, hashes or connection details. Docker and Kubernetes preserve the same structured conflict through psql error transport. Complete account data skips source import, without retroactive deletion of local accounts. Missing roles or ordinary password credentials reject the current transaction. Historical associations without an eligible account cannot authenticate and no longer block SU preflight merely because the association row remains.
Start with sanitized deployment output and database/workload readiness. Do not paste password hashes, signing keys, DSNs or private environment files into logs. The following query only reports schema presence:
docker exec nexent-postgresql psql -U root -d nexent -c \
"SELECT to_regclass('nexent.user_account_t') AS accounts;"
# Kubernetes equivalent:
kubectl --namespace nexent exec deployment/nexent-postgresql -- psql -U root -d nexent -c \
"SELECT to_regclass('nexent.user_account_t') AS accounts;"Use your configured DB/user. Schema presence alone cannot establish completion.
The Shell entry logs its shared data inspection as counters and unresolved IDs.
Inspect [account-migration][preflight], [source], [snapshot], [worker],
[result], [committed] and [cleanup] to identify the stage and outcome. No credential values
are logged. A skipped run explicitly says source cleanup was not attempted;
a remote source requires manual cleanup, while preview and failure retain it.
| Symptom | Checks and recovery |
|---|---|
| Account table exists but historical passwords were never imported | Check whether 001 or the new application was started manually first. Stop identity writes and recover a consistent pre-upgrade target backup, including original membership roles/emails, then run migration before application SQL. Reconcile newer local credentials before backup recovery. The script reports unresolved IDs and stops without automatically repairing or deleting existing account data |
| SU bootstrap reports an existing SU membership or missing account profile | Verify original ID and membership/account consistency. Deleting only account rows leaves tenant ownership behind. Restore the matching target backup; do not replace the owner with a new UUID or delete memberships to bypass preflight |
| Original source container or data directory missing | For Docker, set SUPABASE_DATA_DIR to the original PostgreSQL data directory or check ROOT_DIR. Restore that data from backup if needed, then retry deployment. Check the configured container name if another candidate exists. For Kubernetes or remote sources, restore the original workload or verify the remote endpoint |
| Source stopped, no published port, or readiness timeout | The Shell coordinator starts an existing source and the worker uses internal endpoints. Check source readiness, both Docker bridge networks or Kubernetes services/network policies, and adjust --wait-timeout within its supported range |
| Password unavailable or PostgreSQL authentication fails | Use the original SUPABASE_POSTGRES_PASSWORD and target credentials. Check the selected environment file and exported values; automatic local mode can read original container/pod settings. Changing a new container environment does not reset the old DB password |
| Target account/role/email conflict, ambiguous source IDs, unsupported date or hash | Resolve the actual conflicting legacy metadata or recover its backup. Import must preserve stable IDs and supported hashes; do not invent UUIDs, roles or replacement passwords. Failure leaves the original target schema/data available for a clean retry |
ModuleNotFoundError for dotenv or psycopg2 |
Rebuild/select the matching backend image. The worker requires its packaged database driver; installing packages on the host does not fix the image |
Python path contains C:/c/... or /opt/C:/Program Files/Git/... |
Use the current migrate.sh entry, which never launches host Python. Docker/kubectl container paths retain scoped exclusions; host filenames passed to native kubectl are converted explicitly |
Missing migration.py, migrate.py, legacy_stack.py, or old version/pending-account errors |
Use a consistent checkout/package. Current host entry is migrate.sh; the image contains its Python worker; remove stale launch commands instead of restoring deleted migration environment variables or phase files |
| SQL bootstrap or application startup fails | Inspect Config/bootstrap Job logs and the normal SQL runner output. Fix the failing schema/service prerequisite and retry deployment. A failed new credential transaction rolls back its table/profile changes; an existing completed table is preserved |
| Cleanup incomplete after migration committed | Accounts are already committed. Correct Docker/kubectl access and finish exact-resource cleanup, retaining volumes/backups. Do not drop the account table or rerun a source restore to undo a cleanup warning |
| Kubernetes source has missing/unbound/ephemeral storage | Verify the source Deployment and actual PVC/PV bindings. Preserve original storage definitions and restore its original bound storage before retrying; never substitute a new empty claim |
| Kubernetes rejects a legacy single Helm release | Resolve infrastructure/application resource ownership and the two-release layout first. Account import does not migrate Helm ownership; keep original values, Secrets and storage for that operation |
Helm reports secrets.jwtSecret is required for local authentication |
Supply the original signing key through private nexent-common.secrets.jwtSecret values. The deployment script generates/reuses it; a shell variable alone does not populate direct Helm values |
Do not delete user_account_t after a completed migration to force another import.
Its role/email fields may now be the only canonical copies. A partially upgraded
older target requires backup recovery or an explicit, reviewed data repair.
Verify original password login, wrong/unconfirmed/unavailable user rejection, original UUID/tenant/role/group and business ownership, password-change JWT revocation, existing API keys and revoked keys, and fresh/rerun SU bootstrap. Verify provider bindings and CAS sid/single logout against actual providers before production cutover. Local mocks do not establish external-provider acceptance.
Before new identity/password writes, application rollback requires restoring the target PostgreSQL backup and original application configuration together against the preserved source. The old application needs membership email/role columns; rolling back only its image is insufficient. After cutover writes, reconcile new accounts, password and profile changes before restoring an older backup. SQL replay is a forward upgrade and cannot recover intentionally deleted account data.
Local verification uses AUTH-LOCAL-D5-001 for real PostgreSQL schema/credential
atomicity, SQL replay and source lifecycle, and AUTH-LOCAL-D2-001, API-005,
API-006 for native HTTP/authentication behavior. deploy/tests/test_common.sh
guards Shell-worker-before-application execution order and failure propagation.
NEXENT_AUTH_TEST_DSN must identify an isolated instance allowed to create/drop
test databases. Kubernetes command/storage unit tests and Shell checks are separate from actual
cluster Job verification; report the executed cluster/context and fixture scope.