_id if present, otherwise id, decided per collection. A non-empty collection with neither is treated as invalid and aborts the run (override with --skip-invalid). Empty collections are skipped.Migrate an existing Prisme.ai installation from MongoDB to PostgreSQL for the three structured-data stores (users, permissions, collections).
users, permissions and collections — from MongoDB to PostgreSQL. It applies when you already run Prisme.ai on Mongo and want to move to Postgres (the recommended engine for new deployments).
The approach is a data reimport, not a live replication: you stand up a fresh Postgres-backed install, dump the Mongo databases, and load each dump into its Postgres counterpart with a migration script that upserts every document by id.
users and permissions tables on startup, and the collections schema is generated by the MongoDB platform runtime and applied as collections.sql. The migration script only loads data into those existing tables and adapts each value to their real column types; it does not design the schema.users, permissions and collections. This migration moves all three at once. See Databases / PostgreSQL.Deploy the Postgres-backed platform
Import the collections SQL schema
collections schema from the source runtime and apply it so every collection table exists with the correct types and constraints.Dump the MongoDB databases
mongodump the users, permissions and collections databases.Run the migration script
mongo_to_postgres.py, once per database.mongodump — the source dumps.
psql) — see installing the client.
scripts/mongo_to_postgres.py — the import script (shipped with the platform repository).collections.sql — the collections schema as SQL DDL, generated from your source (Mongo-backed) runtime (see Generating the collections schema). It carries every collection table with its real types, constraints and defaults.postgresql (see Databases / PostgreSQL — Configuration and Helm install — PostgreSQL).
Then verify the platform is healthy before importing any data:
prismeai-api-gateway, prismeai-workspaces, prismeai-events, prismeai-runtime, and the apps.users and permissions schemas are initialized. The backends create their own tables on first startup:
prismeai-api-gateway auto-creates the users tables (accounts, organizations, memberships, roles, API keys…).prismeai-workspaces auto-creates the permissions tables (workspace-level RBAC).users and permissions, the collections tables are not created by a backend on startup — they are provisioned lazily as platform workspaces are imported. To get the schema the clean way without replaying that whole path, generate it from your source runtime and apply it to the target.
collections.sql from a running runtime pod:
Exec into a runtime pod
Fetch the schema
$INTERNAL_API_KEY is the runtime’s internal API key — already present in the pod’s environment, which is why the call is made from inside the runtime. The /sys/collections/schema route is internal-only and rejects any request without the matching key. Copy the resulting collections.sql out of the pod to where you will run psql (e.g. kubectl cp <pod>:/path/collections.sql ./collections.sql).
$COLLECTIONS_PG_URL must include the database name in the URL (e.g. postgres://user:pass@host:5432/collections?sslmode=require). With psql, a separate -d name flag replaces the whole connection string from the URL — put the database in the URL instead. See psql troubleshooting../mongodump/<db>/<collection>.bson (plus .metadata.json sidecars) — exactly the layout the migration script expects.
--mongo-db at the source Mongo database (a sub-directory of the dump) and --pg-database at the target Postgres database:
--pg-database overrides the database name in --pg-dsn, so you can keep a single generic $PG_URL (host / user / password) and only change the target database between runs.
-> upserted 1240 row(s). Empty and excluded collections are reported as skipped.Loads into the existing schema
users, permissions) or by collections.sql — the script reads its real column types, defaults and constraints from information_schema and coerces every Mongo value to match: strings to timestamptz, epoch numbers to dates, objects to jsonb, string arrays to text[], and so on. This is the reliable path, and the reason you should provision collections.sql up front.For a collection whose table is missing, the script falls back to inferring a schema from the data it reads and creating the table itself. Treat this as a last resort: the inferred types are guessed from sample values and can be wrong, causing failures at insertion or at read time (see the warning in Step 2). Provision the real schema for every important table instead of relying on this fallback.Idempotent — safe to re-run
INSERT … ON CONFLICT (pk) DO UPDATE), so ids are preserved and re-running never duplicates rows nor errors. If a run is interrupted, just run it again.Primary key detection
_id if present, otherwise id, decided per collection. A non-empty collection with neither is treated as invalid and aborts the run (override with --skip-invalid). Empty collections are skipped.camelCase → snake_case column names
snake_case to match the platform ORM (e.g. authData → auth_data, expiresAt → expires_at); _id is kept as-is. Use --column-naming preserve to keep the original Mongo names instead.NOT NULL defaults
NOT NULL column has no value in the Mongo document, the script applies the table’s real Postgres DEFAULT (any type), falling back to [] for arrays and false for booleans — matching the ORM defaults that Mongo documents don’t store.Excluding rows or whole collections
--exclude skips data at two levels:collection:field=value — skip matching documents. Some rows are seeded by the API at startup and would collide with the dump on a secondary unique constraint; the defaults skip those platform-workspace rows.collection (bare name) — skip an entire collection, before it is even read. Useful for temp/cache collections you don’t want in Postgres (e.g. --exclude '<workspaceId>/atlasReportsTemp', using the decoded name)."Users", "AccessTokens"). Quote them in psql queries.psql connects to the wrong server
connection to server on socket "/tmp/.s.PGSQL.5432" failed means psql ignored your connection URL and fell back to the local socket. This happens when you pass both a URL and a bare -d dbname — the -d replaces the whole connection string. Put the database in the URL instead:echo "$COLLECTIONS_PG_URL") — an empty value produces the same fallback.null value in column … violates not-null constraint
NOT NULL column received no value and has no usable default. The script auto-fills DB defaults, empty arrays and false booleans; anything else means a genuinely missing required field or a type that couldn’t be coerced. The script prints a one-line diagnostic naming the field, column and reason just before the failure — use it to identify the field.duplicate key value violates unique constraint
--exclude. The platform-workspace roles are excluded by default (roles:subjectId=platform).Aborting — invalid collection(s)
_id nor id, so no stable key exists to upsert on. Inspect the collection; if it is genuinely disposable, re-run with --skip-invalid.