Adoption guide

draft v0.1If you're considering an exporter or importer for your app, this is the page that argues it's worth your time.

Two apps have committed: Prism and Sheaf. Below are full mapping tables for each app's existing export shape against OpenPlural v0.1, followed by shorter notes for Simply Plural, PluralKit, and Plural Star.

Status legend

The same four tags appear in the Status column of every Prism/Sheaf row below.

direct The source field maps 1:1 to an OpenPlural field with the same shape.

transform Lossless conversion (e.g. enum rename, string-to-array, decryption on export).

normalize Source inlines data that OpenPlural splits into a sibling array (e.g. inline member_idsgroup_memberships[]).

extensions.* Source-specific data preserved under a namespaced key. Lossless but opaque to other apps.

Headline adopters

About 30 fields each. Most rows are direct; the interesting cases are normalize and extensions.*.

Prism Adopter

.prism encrypted JSON envelope (V3 container). Local-first Flutter app. Source of truth: lib/features/data_management/models/export_models.dart.

Mappings here are maintainer-provided pending a public sample export fixture.

Prism fieldOpenPlural targetStatus
formatVersion(envelope) — replaces with openplural_versiontransform
appNameproducer.appdirect
versionproducer.app_versiondirect
exportDateexported_atdirect
systemSettings.systemNameSystem.namedirect
systemSettings.systemDescriptionSystem.descriptiondirect
systemSettings.accentColorSystem.colordirect
systemSettings.avatarDataAsset (kind: avatar) + System.avatar_asset_idnormalize
systemSettings.feature toggles + themeextensions.prism.settingsextensions.*
headmates[].id, name, displayName, pronounsMember.id, name, display_name, pronounsdirect
headmates[].age, birthday, notesMember.age, birthday, descriptiontransform
headmates[].profilePhotoDataAsset (kind: avatar) + Member.avatar_asset_idnormalize
headmates[].emoji, customColor, isAdminextensions.prism.*extensions.*
headmates[].pluralkitUuid, pluralkitIdSourceRef (app: "pluralkit")transform
headmates[].proxyTagsJsonMember.proxy_tags[]transform
headmates[].displayOrderMember.sort_orderdirect
headmates[].parentSystemId(via System.parent_system_id)transform
frontSessions[].startTime, endTimeFrontPeriod.started_at, ended_atdirect
frontSessions[].headmateIdFrontPeriod.assignments[0].member_idnormalize
frontSessions[].notes, confidence, qualityFrontAssignment.note, confidence, mooddirect
frontSessions[].sessionType (sleep)FrontPeriod.status: "sleep" + extensions.prism.sessionTypetransform
frontSessions[] legacy co-fronter JSONextensions.prism.legacyCoFrontersextensions.*
sleepSessions[]FrontPeriod with status: "sleep"transform
frontComments (newer, time-anchored)FrontCommentdirect
frontComments (legacy, sessionId-anchored)FrontComment with front_period_idtransform
memberGroups[] (id, name, color, emoji, parentGroupId)Groupdirect
memberGroupEntries[]GroupMembershipdirect
customFields[]CustomFieldDefinitiondirect
customFieldValues[]CustomFieldValuedirect
notes[]Notedirect
conversations[]chat.conversations[]direct
messages[]chat.messages[]direct
mediaAttachments[]Asset + chat.attachments[]normalize
polls[], pollOptions[]polls module (v0.2)extensions.*
habits[], habitCompletions[]habits module (v0.2)extensions.*
reminders[]reminders module (v0.2)extensions.*
friends[]sharing module (v0.2)extensions.*

Sheaf Adopter

/v1/export JSON v2, plus an async zip export with image bytes. FastAPI/PostgreSQL with application-level encryption (decrypted on export). Source of truth: docs/apps/sheaf.md.

Sheaf fieldOpenPlural targetStatus
version: "2"(envelope) → openplural_version: "0.1"transform
system.id, name, description, tagSystem.id, name, description, tagdirect
system.avatar_urlAsset (uri-only) + System.avatar_asset_idnormalize
system.colorSystem.colordirect
system.privacySystem.privacy.visibilitydirect
system.date_format, replace_fronts_defaultextensions.sheaf.*extensions.*
system.delete_confirmation, safety, retentionsafety module (when v0.2 lands); otherwise extensions.sheaf.*extensions.*
system.note (decrypted)extensions.sheaf.note (separate from description; encrypted at rest in Sheaf)extensions.*
members[].idMember.iddirect
members[].name (decrypted)Member.namedirect
members[].display_nameMember.display_namedirect
members[].description (decrypted)Member.descriptiondirect
members[].pronounsMember.pronounsdirect
members[].pluralkit_idSourceRef (app: "pluralkit", collection: "members")transform
members[].avatar_urlAsset + Member.avatar_asset_idnormalize
members[].colorMember.colordirect
members[].birthdayMember.birthday.value (Sheaf stores either "MM-DD" or "YYYY-MM-DD"; derive precision + year_visible accordingly)transform
members[].privacyMember.privacy.visibilitydirect
members[].is_custom_frontMember.is_custom_frontdirect
members[].emojiextensions.sheaf.emoji (Prism has an analogous field; candidate for a shared optional in a later spec)extensions.*
members[].note (decrypted)extensions.sheaf.note (separate from description; encrypted at rest in Sheaf)extensions.*
members[].created_atMember.created_atdirect
fronts[].id, started_at, ended_atFrontPeriod.id, started_at, ended_atdirect
fronts[].member_idsFrontPeriod.assignments[] (one per id, role: "member")normalize
fronts[].custom_status (decrypted)FrontPeriod.status (free-text)direct
groups[].id, name, description, colorGroup.id, name, description, colordirect
groups[].parent_idGroup.parent_group_iddirect
groups[].member_idsGroupMembership[] (one per id)normalize
tags[].id, name, colorTaxonomyTerm.id, name, color (kind: "tag")transform
tags[].member_idsTaxonomyAssignment[] (subject_type: "member")normalize
custom_fields[].id, name, field_type, options, order, privacyCustomFieldDefinition (options accepts string[] | Record<string, unknown> | null since Sheaf stores it as JSONB dict | None)transform
custom_fields[].values[] (member_id, value)CustomFieldValue (subject_type: "member")normalize
journals[].id, member_id, title, body, visibility, author_member_ids, created_at, updated_atNotedirect
journals[].image_keys + async zip images/<key>Asset + Note.attachment_asset_idsnormalize
messages[] (board posts: system board + per-member walls)boards.posts[] (with caveats; see separate issue on parent_post_id)transform
messages[].parent_message_id (single-level reply pointer)extensions.sheaf.parent_message_id until BoardPost.parent_post_id landsextensions.*
polls[], poll.options[], poll.votes[], poll.events[]extensions.sheaf.polls until the polls module lands in v0.2extensions.*
reminders[]extensions.sheaf.reminders until the reminders module lands in v0.2extensions.*
sync uploaded_files[] inventory without bytesextensions.sheaf.uploaded_files unless paired with the async zipextensions.*
revisions[] (journal/member-bio edit history)extensions.sheaf.revisions until OpenPlural grows a revision-history shapeextensions.*
watch_tokens[] + channels[]extensions.sheaf.watch_tokens until OpenPlural grows a notifications/export moduleextensions.*

The sync JSON export is now good enough for systems, members, fronts, groups, tags, custom fields, and journals. For portable assets[], prefer Sheaf's async zip export over bare /v1/export: the zip includes the actual images/<key> blobs, while the sync JSON only has avatar URLs and uploaded-file inventory metadata.

Adoption notes for other apps

Shorter pointers — full mapping tables come once each app commits to OpenPlural support.

Simply Plural

Pre-discontinuation export. Custom fronts → Member.is_custom_front: true. Privacy buckets preserved under privacy.source.simply_plural. Chat decrypted on export. channels+chatMessages map into the chat module; boardMessages map into the boards module as BoardPost.

PluralKit

Switch logs → front_events[]; periods optionally derived. Flat groups; rich per-record privacy preserved on the privacy fragment. Proxy tags map to Member.proxy_tags[]; autoproxy/account links wait for the proxy module in v0.2.

Plural Star

Tiered fronting → FrontPeriod with front_role per tier and source_kind: "tiered". Channels map to the chat module; noteboards map to the boards module as BoardPost with pinned. Filesystem backup format v1.2 maps cleanly to v0.1 core.

Maintainer guidance

Four things worth getting right when you build an exporter or importer.

One exporter beats N converters

If you map your internal records to OpenPlural's core, you're done — every other app's importer handles the rest. Pairwise converters are how you end up maintaining nine of them.

Partial imports are still wins

If your app doesn't have chat, polls, custom fields, or groups, importing the rest and reporting the skip is much better than refusing the file. The user's other data still moves.

Provenance survives migrations

Original app IDs in source_refs are what lets users (and future sync tools) reconcile records after a round-trip. Without them, every import looks like a fresh dataset.

Extensions are cheap insurance

Source-specific fields under namespaced extensions cost almost nothing to write but preserve data the user might want later — including data another app may eventually understand.

01
Adoption path

Start with exporter support for systems, members, front history, groups, custom fields, notes, and assets. Add taxonomy when your app has role-like labels, tags, or reusable profile categories. Treat chat, polls, reminders, proxy settings, and friend sharing as optional modules — you can preserve them even when the target app won't show them.

02
Reporting

Always emit an ImportResult after a run. Compare "247 imported, 12 degraded, 158 retained as archive data, 0 failed" to "import complete." The first one tells the user what actually happened.