Sangriadocs
Data migrations

Data migrations

The source-agnostic migration framework shared by every import source.

Sangria can import a workspace's history from an external chat tool. The framework itself is source-agnostic — a second source reuses the same primitives instead of inventing a parallel table. Shared code lives in convex/migration/idMap.ts; everything specific to one source lives in its own sibling folder (e.g. convex/migration/mattermost/).

A migration is one-time, destructive-adjacent tooling (it creates real workspaces, users, and messages). Every import runs against the local dev Convex deployment first — the throwaway/dry-run pass is the verification step. Production is only touched at cutover, after row counts and a manual spot-check both pass.

Why a separate framework, not schema changes

The core product schema (users, channels, messages, …) stays completely untouched by migration concerns — no mattermostId fields scattered through it for what is one-time historical import. Everything external-id-shaped lives in one standalone table instead.

migration_id_map

One row per migrated entity, indexed by_source_kind_externalId:

FieldPurpose
sourceWhich external system ("mattermost" today).
kindWhat kind of entity ("user", "channel", "conversation", "message", "workspace", …).
externalIdThe source system's own identifier for the row (a Mattermost username, a team:channel pair, …).
sangriaIdThe Sangria id it maps to.

The importer reads this before creating anything — "have I already imported this row" makes every import mutation idempotent and safe to re-run over already-processed data, and it's the traceable record for spot-checking an entity's origin later. It's also the whole reason a migration is cheap to undo: drop the rows for one source/workspace and nothing else in the schema knows a migration ever happened.

account_claim_tokens

A general "claim/set up this account" primitive (not tied to any one source): userId, an unguessable token, expiresAt, consumedAt. A pre-provisioned user gets one minted at import time; visiting /claim-account?token=… lets them set a password without Sangria ever having had one. See convex/account/claim.ts.

If an imported user's email already matches an existing Sangria account, the importer merges — it adds them to the target workspace and never mints a claim token, since they already have a working sign-in.

Sources built on this framework

On this page