Kompass-RFC-001 — Identity and Organization Unification
- Status: Ratified 2026-06-25 (owner authorization — "vamos destravar"). Design decisions complete; the two formerly-pending Open Questions are resolved below. Ratification unblocks defining downstream identity entities and building the Phase 0 schemaAPIs (tracked in `servicesfoundationidenginebacklog
ID-253 andservicesfoundationkompassenginebacklog` KOMPASS-001). The destructive backfill steps (Person collapse on real CrescerKoder/Vivver data) remain gated behind dry-run + manual review per §Risks — ratification does not auto-run them. - Date: 2026-04-11 (drafted) · 2026-06-25 (ratified)
- Author: Koder Team
- Depends on:
id-v2-RFC-002-data-model.md - Extended by:
kompass-RFC-002— attachable identifiers (user_identities) + per-org identity policy. Supersedes theEmailentity below (email becomes auser_identitiesrow,kind='email'). - Blocks: all
erp-RFC-NNNdocuments,kompass-RFC-003onward (RFC-002 is the identifiers RFC; see §Open questions) - Scope:
services/foundation/id,services/foundation/kompass, legacy org chart app (~/dev/organograma/),Crescer/c-corp/modules/seguranca
Summary
Today, four independent systems answer the question "who is a person and which organizations do they belong to," and they disagree with each other:
services/foundation/id(Koder ID v2) definesuserswithtenant_idas partition key. One human = N user records across tenants.services/foundation/kompassdefinesMemberwithOrgID uuid.UUIDon every row. A user belonging to N orgs = N member records.- The legacy org chart app (
~/dev/organograma/) OrgML models a person as a single entity with a multi-company badge ([CKV]= Crescer + Koder + Vivver), vacancies as first-class, and PJ/CLT as a first-class flag. It has the richest domain model but lives outside the monorepo. Crescer/c-corp/modules/segurancare-implements its own IAM (usuario,perfil,permissao) with Google auth and domain-restricted login. It is running in production for Crescer/Vivver.
These four cannot all be right. This RFC picks one identity model, describes the migration, absorbs OrgML as the canonical DSL for org structure import/export, and retires the other three implementations.
This is Phase 0 of the ERP track per erp-RFC-000. No downstream RFC is allowed to define new entities that reference identity until this RFC lands.
Motivation
The concrete failure case
Rodrigo Pereira de Mendonça is director of Crescer, Koder, and Vivver simultaneously. He has one CPF, one e-mail he uses for work, one password, one MFA device, one face, one legal signature. He manages across the three companies every day — approving bids for Crescer, signing hires for Vivver, deploying infrastructure for Koder.
Under the current model, representing this person requires:
- Three rows in
services/foundation/idusers— one per tenant (tenant_id = crescer,tenant_id = koder,tenant_id = vivver). Each row has its ownid, its own password hash column, its own session history, its own MFA enrollment. When he changes his password, it changes in one tenant and diverges from the other two. - Three rows in
services/foundation/kompassmembers— one per org. Each row has its owndisplay_name,email,phone,photo_url, and optionaluser_idlink back to one of the three ID users. Updating his phone number means updating three rows. - Three sessions to switch between — no single sign-on across his three tenants in a way that lets him say "show me all my tasks across all my orgs." Switching tenants is actually switching identities.
- One row in
~/dev/organograma/OrgML.md—@ Rodrigo Pereira de Mendonça [CKV]. This is the source of truth in practice, because it is the one Rodrigo edits by hand onorganograma.koder.dev. The other three models drift from it.
Why OrgML has the right model and the others don't
OrgML was written after everything else, under pressure from the actual org chart Rodrigo had to maintain. It made concessions the earlier systems did not:
| Concept | OrgML | Koder ID v2 | Kompass | c-corp seguranca |
|---|---|---|---|---|
Person is global (one row per human) |
✅ implicit | ❌ tenant-partitioned | ❌ per-org Member |
❌ per-org usuario |
| Person belongs to multiple tenants | ✅ [CKV] badge |
❌ | ❌ | ❌ |
| Vacancies are first-class | ✅ !@, ! with xN |
— | ❌ | — |
| Employment type (CLT/PJ) | ✅ {pj} |
— | ❌ | — |
| Location per member | ✅ (Cidade - UF) |
❌ user-level only | ❌ member-level, free-form | — |
| Role inline | ✅ *Cargo* |
— | partial (separate role entity) |
✅ |
| Hierarchy (units containing subunits) | ✅ numeric levels | — | ✅ | — |
| Color/metadata per unit | ✅ {#hex} |
— | ❌ | — |
OrgML is missing: strong identifiers (no IDs, names are keys), auth (no password/credential), authorization (no permission model), machine-readability beyond a custom parser.
The other three are missing: multi-tenant person, vacancy, employment type, location.
The answer is neither "adopt OrgML wholesale" nor "fix Kompass to match OrgML within its current schema." The answer is to split the problem: identity goes to services/foundation/id as a global Person, org structure goes to services/foundation/kompass as Membership, and OrgML becomes the canonical text format for importing/exporting the structure.
Proposed model
Entities
erDiagram
Person ||--o{ Membership : "belongs via"
Tenant ||--o{ Membership : "contains"
Tenant ||--o{ Unit : owns
Unit ||--o{ Unit : "parent of"
Unit ||--o{ Membership : "assigned to"
Role ||--o{ Membership : "granted to"
Unit ||--o{ Vacancy : "has open"
Role ||--o{ Vacancy : "for role"
Person ||--o{ Credential : authenticates
Person ||--o{ Email : has
Person ||--o{ Phone : has
Person {
ulid id PK
string canonical_name
string preferred_name
string locale
string timezone
jsonb metadata
}
Tenant {
ulid id PK
string slug
string display_name
string country
jsonb metadata
}
Membership {
ulid id PK
ulid person_id FK
ulid tenant_id FK
ulid unit_id FK
ulid role_id FK
string employment_type "clt|pj|intern|apprentice|volunteer|contract"
string location_city
string location_state
string location_country
timestamp started_at
timestamp ended_at
jsonb metadata
}
Unit {
ulid id PK
ulid tenant_id FK
ulid parent_id FK
string name
string color_hex
int level
jsonb metadata
}
Role {
ulid id PK
ulid tenant_id FK
string name
string category
jsonb permissions
}
Vacancy {
ulid id PK
ulid tenant_id FK
ulid unit_id FK
ulid role_id FK
string kind "owner|member"
int quantity
string employment_type
string location_city
string location_state
timestamp opened_at
timestamp filled_at
jsonb metadata
}
Credential {
ulid id PK
ulid person_id FK
string kind "password|webauthn|totp|oidc_subject|api_key"
bytes secret
timestamp expires_at
}
Email {
ulid id PK
ulid person_id FK
string address
bool verified
bool primary
}
Phone {
ulid id PK
ulid person_id FK
string e164
bool verified
bool primary
}Invariants enforced by this model
Personis tenant-less. It has notenant_idcolumn, never. It is the global identity of a human.Credential,Email,Phonebelong toPerson. One password, one primary email, one MFA device for the human — not three.Membershipis the only tenant-scoped table that references a person. Everything tenant-specific about a person lives in Membership: which unit, which role, CLT or PJ, where they work from, when they joined, when they left. Multiple memberships per person are normal; that is the whole point.Vacancyis first-class. It is not a flag on Membership. It has its own lifecycle (opened → filled → closed), its own approval chain, its own reporting.Unithas a recursiveparent_id. Unlimited depth. OrgML's1[DIRETORIA] → 2[Gerência] → 3[Departamento] → 4[Subseção]translates to rows with level 1..4 and aparent_idchain.Roleis tenant-scoped. "Diretor" in Crescer is not the same Role as "Diretor" in Vivver, even if they share a name. The model could in principle support a global role catalog, but it does not — tenant isolation of role definitions matters for permissions and compliance.EmploymentTypeis an enum, not free-form text.clt,pj,intern,apprentice,volunteer,contract. Extensible by adding enum values in a later migration; never by free-form.
Authentication and session
Authentication produces a Person-scoped session. That session can then be "activated" against one or more Tenants — the session carries a current-tenant pointer that the user can change at any time without re-authenticating. Logout ends the session; it does not end "some of the tenants."
This matches how every SaaS with multi-tenant membership does it: Slack (one login, pick a workspace), GitHub (one login, pick an org), Notion (one login, pick a workspace). Koder was the odd one out.
Migration from the current Koder ID v2 schema
Current services/foundation/id/v2 has users(id, tenant_id, username, email, …) with UNIQUE (tenant_id, username) and UNIQUE (tenant_id, email). Migration:
- Introduce
personstable with the global shape above. - For each
(email, email_verified=true)pair across the existinguserstable, insert onepersonsrow if one does not already exist for that e-mail. For verified duplicates, collapse into a single person (assume same human). For unverified duplicates, flag for manual resolution — do not silently merge. - Rewrite
usersintomembershipsby translatingtenant_id, username, … → memberships(person_id, tenant_id, …). Everything the oldusersrow carried that was tenant-specific (e.g. last login from this tenant, per-tenant role assignments) moves tomembershipsor a sibling table. Everything that was identity-global (primary email, password hash) moves topersonscredentialsemails. - Credentials are deduplicated. If three rows in the old
usersshared the same password hash AND the same verified primary email, they were the same human — move onecredentialsrow topersonsand drop the others. audit_logentries are preserved verbatim with bothold_user_idand the new(person_id, tenant_id)coordinate. Never rewrite history.
The migration is destructive in the sense that three old users collapse into one Person, but it is not destructive for audit or billing — the migration records the mapping so that any old identifier can be resolved to the new coordinates.
Migration from the current Kompass schema
Current services/foundation/kompass/backend/internal/models/member.go has Member { OrgID uuid.UUID; UserID *uuid.UUID; FirstName, LastName, DisplayName, Email, Phone, Address, City, …} — a wide denormalized row with every personal detail duplicated per-org.
Migration:
- For each
Memberrow, look up thePersoncorresponding to thatMember.UserID(via the Koder ID v2 migration above). IfUserIDis null, create a newPersonfrom the member's name/email. - Copy the member's
FirstName, LastName, DisplayName, Email, Phone, …into thePerson— but only if the Person is new or the Member row is newer than what the Person currently has. Later rows win; ties broken byupdated_at. - Create a
Membershiprow with(person_id, tenant_id=OrgID, unit_id, role_id, employment_type, location_city, location_state, started_at=JoinDate, ended_at=ExpiresAt). - Drop the denormalized fields from
Member→Membership. Keep only the coordinate fields (person, tenant, unit, role, lifecycle). - OrgML import becomes a one-shot: parse
~/dev/organograma/OrgML.md, upsertPersonrows by name, upsertMembershiprows by(person, tenant)coordinate, upsertVacancyrows for!@/!entries. This run seeds the three real tenants (Crescer, Koder, Vivver) with the real-world org structure as maintained by Rodrigo.
Migration from c-corp/seguranca
c-corp has its own usuario, perfil, permissao tables with Google OAuth domain restriction. Migration:
- Every
usuariorow becomes aPersonif one does not already exist by canonical email. - Every
perfilbecomes aRolein the appropriate Kompass tenant (Crescer or Vivver depending on domain). - Every
permissaois mapped to Kompass's permission model — or, if the mapping is ambiguous, flagged for manual review and parked in a temporarylegacy_permissionstable keyed by(membership_id, legacy_code). - c-corp stops using
modules/segurancaand starts using Koder ID OIDC viaservices/foundation/id. c-corp frontend adds a Koder ID login screen as the only auth path. - The
modules/segurancacode is deleted from c-corp once the last user has migrated. Soft-deprecation period: 60 days after the Koder ID integration ships.
OrgML as canonical DSL
OrgML becomes a first-class format in the stack:
- Spec lives at
meta/docs/stack/specs/orgml.md, ported from~/dev/organograma/SPEC.md. Keep the semantic additions that the RFC model introduces (EmploymentType enum values, Vacancy lifecycle, multi-tenant membership via company tags). - Go parser at
services/foundation/kompass/internal/orgml/— port the JS parser from~/dev/organograma/index.htmlinto Go. Expose as a library plus a CLI atservices/foundation/kompass/cmd/kompass-orgml/withimport,export,validate,diffsubcommands. - Dart parser at
apps/mosaic/lib/orgml/— second port, for client-side rendering when Mosaic wants to let users paste OrgML directly. - The org chart app (
organograma.koder.dev) becomes a thin client that reads/writes OrgML via Kompass's API. The standaloneapi.py+OrgML.mdpair on the k.lin filesystem is retired; the file becomes a view of the Kompass data, generated on demand and synced back on POST. This preserves the "edit a textarea, hit save, see the chart" workflow that Rodrigo likes, while making Kompass the source of truth. - Import/export round-trip is tested. A Kompass tenant exports to OrgML, imports back, and the resulting state is identical modulo field ordering. This is the acceptance test that the OrgML spec and the Kompass model agree with each other.
OrgML extensions added by this RFC (beyond the current spec):
- Date fields — optional
{start: 2024-03-01}{end: 2026-12-31}on members, for employment history tracking. - Credential hints — optional
{email: nome@crescer.net}when a member's email does not match their name for deterministic e-mail generation. - Role reference — optional
*Cargo*today is free-form. The extension allows*ref:role-slug*for referencing a Role by slug instead of inlining the label.
These extensions are backward-compatible: the current OrgML.md parses without them, the Kompass model accepts them when present.
API surface
New endpoints in services/foundation/id:
GET /v1/persons/{id}
GET /v1/persons?email=...
POST /v1/persons
PATCH /v1/persons/{id}
GET /v1/persons/{id}/memberships → proxies to Kompass
GET /v1/persons/{id}/credentials
POST /v1/persons/{id}/credentials
DELETE /v1/persons/{id}/credentials/{cid}
GET /v1/persons/{id}/emails
POST /v1/persons/{id}/emails
GET /v1/persons/{id}/phones
POST /v1/persons/{id}/phones
POST /v1/auth/login → returns session keyed to person, not tenant
POST /v1/auth/tenant/activate → sets current tenant in session
GET /v1/auth/me → returns person + list of tenantsNew endpoints in services/foundation/kompass:
GET /v1/tenants/{id}/memberships
POST /v1/tenants/{id}/memberships
PATCH /v1/tenants/{id}/memberships/{mid}
DELETE /v1/tenants/{id}/memberships/{mid}
GET /v1/tenants/{id}/units → tree, optional ?depth=N
POST /v1/tenants/{id}/units
GET /v1/tenants/{id}/vacancies
POST /v1/tenants/{id}/vacancies
PATCH /v1/tenants/{id}/vacancies/{vid} → mark filled/closed
POST /v1/tenants/{id}/orgml/import → bulk upsert from OrgML text
GET /v1/tenants/{id}/orgml/export → full tenant as OrgML text
POST /v1/tenants/{id}/orgml/validate → parse without applying
GET /v1/tenants/{id}/orgml/diff → compare uploaded OrgML to current stateExisting endpoints change meaning but not shape — /v1/users/{id} becomes a compatibility shim that resolves (tenant, user) to (person, membership) for 180 days, then is deleted.
Compatibility and retirement plan
gantt
title Identity unification rollout
dateFormat YYYY-MM-DD
section Schema
Introduce Person/Membership tables :a1, 2026-04-15, 14d
Backfill from id-v2 users :a2, after a1, 10d
Backfill from kompass members :a3, after a2, 10d
Backfill from c-corp seguranca :a4, after a3, 10d
Backfill from OrgML :a5, after a4, 5d
section API
Ship new /v1/persons and membership APIs :b1, after a1, 21d
Ship compatibility shim for old /v1/users :b2, after b1, 5d
section Clients
Mosaic uses new APIs :c1, after b1, 14d
Org chart app becomes thin client :c2, after b1, 10d
c-corp uses Koder ID OIDC :c3, after b1, 21d
section Retirement
Delete kompass Member denormalized fields :d1, after c1, 7d
Delete c-corp seguranca module :d2, after c3, 7d
Delete id-v2 users compat shim :d3, 2026-10-15, 1dFreeze period: starting when this RFC merges, no new code is allowed to reference users.tenant_id or Member.OrgID for identity purposes. New code uses persons and memberships. The old columns exist for read compatibility until the shim is deleted.
Permissions and authorization
This RFC does not redesign authorization. It reuses whatever permission model services/foundation/id and services/foundation/kompass currently ship, with one structural change:
Permissions are evaluated against Membership, not Person. When Rodrigo asks to approve a bid in Crescer, the authorization check runs against Membership(person=rodrigo, tenant=crescer) — his roles in Crescer, not his roles in Koder or Vivver. This is what lets him be director of three companies without leaking cross-tenant permissions.
Session tokens carry (person_id, active_tenant_id, active_membership_id). Switching active tenant reissues the session with a new active_tenant_id and active_membership_id; the person_id never changes until logout.
Security considerations
- Account takeover blast radius increases. If Rodrigo's password is compromised, the attacker gains access to all three tenants, not one. Mitigation: mandatory MFA for persons with memberships in more than one tenant, enforced at login.
- Cross-tenant data leakage risk in joins. Any query that joins
membershipsmust filter byactive_tenant_idor explicitly acknowledge cross-tenant scope. Kompass's query layer adds a helperwithTenant(ctx, tenantID)that stampsWHERE tenant_id = ?and refuses queries without a stamp. Cross-tenant reporting queries usecrossTenant(ctx, personID)instead — explicit and auditable. - Audit log granularity. Audit log entries now carry
(person_id, tenant_id, membership_id)instead of justuser_id. Migration back-fills theperson_idcolumn; older rows have onlyuser_idand are looked up via the compat shim. - LGPD — right to be forgotten. A deletion request targets a
Person, cascading toCredentials,Emails,Phones, andMemberships. Audit log entries are scrubbed to remove PII but retained for compliance. Cross-tenant deletion is atomic: either all three memberships of Rodrigo are deleted along with his Person, or none. - Multi-tenant membership is consent-based. Adding a
Membershiprequires explicit consent from the Person if the Person already exists. A Tenant admin cannot silently attach a new org to someone else's identity.
Risks
- Data quality in backfill. If the existing
usersrows have low-quality duplicates (typos in emails, mismatched usernames), the Person collapse step mis-merges humans. Mitigation: run the collapse in dry-run mode first, produce a manual-review list, resolve by hand for CrescerVivverKoder (the only three tenants with real data today). - OrgML spec drift. If Kompass adds fields the OrgML parser does not know about, exports become lossy. Mitigation: spec and parser versioning — OrgML headers carry
# orgml-version: 2and the parser rejects unknown fields in strict mode, warns in lenient mode. - c-corp runtime during migration. c-corp is in production for Crescer/Vivver. Replacing its auth backend while running carries risk. Mitigation: dual-path auth for 30 days — Google OAuth and Koder ID both work, feature flag decides which UI is shown. Users are migrated gradually.
- Org chart app regression. The thin-client rewrite of the org chart app (
organograma.koder.dev) must preserve the editing UX exactly. Mitigation: side-by-side deploy of the old and new versions for 14 days on different subdomains; cut over only after explicit sign-off. - Kompass tests break. Every Kompass test that references
Member.OrgIDmust be rewritten. Mitigation: automated refactor pass that replacesMember{OrgID: x}literals withMembership{TenantID: x, PersonID: y}and emits a manual-review list for tests where the rewrite is non-obvious. - Longer sessions = stale permissions. Since sessions span tenants, a permission change in one tenant does not invalidate the session. Mitigation: push permission-change events to a session invalidation channel; sessions refresh their permission cache on every sensitive action.
Open questions
- Should
Person.canonical_namebe mutable? → RESOLVED (2026-06-25): mutable, with audit logging. Immutability is already provided by thePerson.id(ULID) anchor —canonical_nameis an attribute, not an identifier, so making it immutable would only force an "immutable-but-wrong" name after a legal change or gender transition. Mutate with an audit trail; keeppreferred_nameas the everyday display. (Consistent with kompass-RFC-002's thesis: the anchor is immutable, everything attached is mutable.) - Should
EmailandPhonebe separate tables or embedded inPerson? → RESOLVED (2026-06-25): separate tables. Verification state and indexability require it. Furthermore,Emailis superseded by kompass-RFC-002'suser_identities— an email is auser_identitiesrow (kind='email') attached to the Person anchor, carryingverify_methodproofing_levelpurpose/tenant_ctx.Phonestays its own table for now (may fold intouser_identitiesaskind='phone'in a later migration). - How does Membership model acting roles (someone filling in for another for two weeks)? This RFC does not cover it. A follow-up
kompass-RFC-003can introduceMembershipAssignmentor similar. (Renumbered:kompass-RFC-002is the attachable-identifiers + per-org-policy RFC; acting-roles moves to RFC-003.) - How are dependents (children of members, for example in a health plan context) modeled? Out of scope for this RFC. Current Kompass has
FamilyLink; keep it, revisit in a future RFC. - Does Koder ID still need a
tenant_idon theuserstable for legacy reasons? No — the table itself is replaced bypersons+memberships. The old table is dropped at the end of the compat window.
Non-goals
- This RFC does not redesign authorization or permissions beyond the structural point that permissions attach to
Membership. - This RFC does not introduce a global role catalog. Roles remain tenant-scoped.
- This RFC does not decide whether the org chart app (
organograma.koder.dev) is kept or retired beyond requiring it to be a thin client of Kompass; the final call is a product decision. - This RFC does not specify the UI for tenant switching in Mosaic or other apps.
- This RFC does not extend to federated identity (SAML, external IdPs). That is a separate RFC in the
id-v2-RFC-NNNtrack.
Appendix A — Worked example with Rodrigo
Before
services/foundation/id/v2 users table:
id=01HZABC…, tenant_id=crescer, username=rodrigo, email=rodrigo@crescer.net, password_hash=A
id=01HZDEF…, tenant_id=koder, username=rodrigo, email=rodrigo@koder.dev, password_hash=B
id=01HZGHI…, tenant_id=vivver, username=rodrigo, email=rodrigo@vivver.com.br,password_hash=C
services/foundation/kompass members:
id=M1, org_id=crescer, user_id=01HZABC…, first_name=Rodrigo, last_name=Mendonça, email=rodrigo@crescer.net, phone=…, photo_url=…
id=M2, org_id=koder, user_id=01HZDEF…, first_name=Rodrigo, last_name=Mendonça, email=rodrigo@koder.dev, phone=…, photo_url=…
id=M3, org_id=vivver, user_id=01HZGHI…, first_name=Rodrigo, last_name=Mendonça, email=rodrigo@vivver.com.br,phone=…, photo_url=…
~/dev/organograma/OrgML.md:
1[DIRETORIA]{#ef4444}
@ Rodrigo Pereira de Mendonça [CKV]Three separate password hashes, three separate photo URLs, three separate phone numbers, three separate session histories. The OrgML is the only place that tells the truth.
After
persons:
id=01J00001, canonical_name=Rodrigo Pereira de Mendonça, preferred_name=Rodrigo
emails:
id=E1, person_id=01J00001, address=rodrigo@crescer.net, verified=true, primary=false
id=E2, person_id=01J00001, address=rodrigo@koder.dev, verified=true, primary=true
id=E3, person_id=01J00001, address=rodrigo@vivver.com.br, verified=true, primary=false
credentials:
id=C1, person_id=01J00001, kind=password, secret=hashed
memberships:
id=MEM1, person_id=01J00001, tenant_id=crescer, unit_id=crescer-diretoria, role_id=diretor, employment_type=clt, started_at=…
id=MEM2, person_id=01J00001, tenant_id=koder, unit_id=koder-diretoria, role_id=diretor, employment_type=clt, started_at=…
id=MEM3, person_id=01J00001, tenant_id=vivver, unit_id=vivver-diretoria, role_id=diretor, employment_type=clt, started_at=…One person, three memberships, one password hash, one session that he activates against whichever tenant he is currently working in.
Appendix B — OrgML → Kompass mapping reference
OrgML syntax → Kompass entity
──────────────────────────────────────────────────────────────────
C = Crescer #10b981 → Tenant{slug=crescer, display_name=Crescer, metadata.brand_color=#10b981}
1[DIRETORIA]{#ef4444} → Unit{level=1, name=DIRETORIA, color_hex=#ef4444, parent_id=null}
2[Gerência Administrativa]{#06b6d4} → Unit{level=2, parent_id=DIRETORIA, name=Gerência Administrativa, color_hex=#06b6d4}
@ Rodrigo Pereira de Mendonça [CKV] → Membership{person=Rodrigo, tenant=Crescer|Koder|Vivver (one row each), role=<unit's owner role>}
- Gabriel Vilete de Souza [V] → Membership{person=Gabriel Vilete de Souza, tenant=Vivver}
@ Carlos Henrique [V] *Coordenador* → Membership{person=Carlos Henrique, tenant=Vivver, role.name=Coordenador}
- Angelo Victor [V] (João Pessoa - PB) → Membership{person=…, tenant=Vivver, location_city=João Pessoa, location_state=PB}
- Cristiano Araújo [C] {pj} → Membership{person=…, tenant=Crescer, employment_type=pj}
!@ Coordenador [V] → Vacancy{tenant=Vivver, unit=<current>, role=Coordenador, kind=owner, quantity=1}
! Técnico de Campo [V] {pj} x3 → Vacancy{tenant=Vivver, unit=<current>, role=Técnico de Campo, kind=member, quantity=3, employment_type=pj}Round-trip invariant: importing OrgML then exporting produces the same OrgML modulo field ordering and whitespace.
Appendix C — Exit criteria checklist
- [ ]
persons,memberships,credentials,emails,phones,units,roles,vacanciestables exist with the schema above - [ ] Koder ID v2 RFC-002 has been amended to reference this RFC
- [ ] Migration scripts for id-v2
users, Kompassmembers, c-corpusuario/perfil/permissao, and OrgML are checked in and dry-run tested against CrescerKoderVivver data - [ ] New `v1persons