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 and servicesfoundationkompassenginebacklog` 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 the Email entity below (email becomes a user_identities row, kind='email').
  • Blocks: all erp-RFC-NNN documents, kompass-RFC-003 onward (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:

  1. services/foundation/id (Koder ID v2) defines users with tenant_id as partition key. One human = N user records across tenants.
  2. services/foundation/kompass defines Member with OrgID uuid.UUID on every row. A user belonging to N orgs = N member records.
  3. 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.
  4. Crescer/c-corp/modules/seguranca re-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/id users — one per tenant (tenant_id = crescer, tenant_id = koder, tenant_id = vivver). Each row has its own id, 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/kompass members — one per org. Each row has its own display_name, email, phone, photo_url, and optional user_id link 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 on organograma.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

  1. Person is tenant-less. It has no tenant_id column, never. It is the global identity of a human.
  2. Credential, Email, Phone belong to Person. One password, one primary email, one MFA device for the human — not three.
  3. Membership is 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.
  4. Vacancy is first-class. It is not a flag on Membership. It has its own lifecycle (opened → filled → closed), its own approval chain, its own reporting.
  5. Unit has a recursive parent_id. Unlimited depth. OrgML's 1[DIRETORIA] → 2[Gerência] → 3[Departamento] → 4[Subseção] translates to rows with level 1..4 and a parent_id chain.
  6. Role is 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.
  7. EmploymentType is 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:

  1. Introduce persons table with the global shape above.
  2. For each (email, email_verified=true) pair across the existing users table, insert one persons row 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.
  3. Rewrite users into memberships by translating tenant_id, username, … → memberships(person_id, tenant_id, …). Everything the old users row carried that was tenant-specific (e.g. last login from this tenant, per-tenant role assignments) moves to memberships or a sibling table. Everything that was identity-global (primary email, password hash) moves to personscredentialsemails.
  4. Credentials are deduplicated. If three rows in the old users shared the same password hash AND the same verified primary email, they were the same human — move one credentials row to persons and drop the others.
  5. audit_log entries are preserved verbatim with both old_user_id and 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:

  1. For each Member row, look up the Person corresponding to that Member.UserID (via the Koder ID v2 migration above). If UserID is null, create a new Person from the member's name/email.
  2. Copy the member's FirstName, LastName, DisplayName, Email, Phone, … into the Person — but only if the Person is new or the Member row is newer than what the Person currently has. Later rows win; ties broken by updated_at.
  3. Create a Membership row with (person_id, tenant_id=OrgID, unit_id, role_id, employment_type, location_city, location_state, started_at=JoinDate, ended_at=ExpiresAt).
  4. Drop the denormalized fields from MemberMembership. Keep only the coordinate fields (person, tenant, unit, role, lifecycle).
  5. OrgML import becomes a one-shot: parse ~/dev/organograma/OrgML.md, upsert Person rows by name, upsert Membership rows by (person, tenant) coordinate, upsert Vacancy rows 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:

  1. Every usuario row becomes a Person if one does not already exist by canonical email.
  2. Every perfil becomes a Role in the appropriate Kompass tenant (Crescer or Vivver depending on domain).
  3. Every permissao is mapped to Kompass's permission model — or, if the mapping is ambiguous, flagged for manual review and parked in a temporary legacy_permissions table keyed by (membership_id, legacy_code).
  4. c-corp stops using modules/seguranca and starts using Koder ID OIDC via services/foundation/id. c-corp frontend adds a Koder ID login screen as the only auth path.
  5. The modules/seguranca code 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:

  1. 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).
  2. Go parser at services/foundation/kompass/internal/orgml/ — port the JS parser from ~/dev/organograma/index.html into Go. Expose as a library plus a CLI at services/foundation/kompass/cmd/kompass-orgml/ with import, export, validate, diff subcommands.
  3. Dart parser at apps/mosaic/lib/orgml/ — second port, for client-side rendering when Mosaic wants to let users paste OrgML directly.
  4. The org chart app (organograma.koder.dev) becomes a thin client that reads/writes OrgML via Kompass's API. The standalone api.py + OrgML.md pair 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.
  5. 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 tenants

New 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 state

Existing 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, 1d

Freeze 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 memberships must filter by active_tenant_id or explicitly acknowledge cross-tenant scope. Kompass's query layer adds a helper withTenant(ctx, tenantID) that stamps WHERE tenant_id = ? and refuses queries without a stamp. Cross-tenant reporting queries use crossTenant(ctx, personID) instead — explicit and auditable.
  • Audit log granularity. Audit log entries now carry (person_id, tenant_id, membership_id) instead of just user_id. Migration back-fills the person_id column; older rows have only user_id and are looked up via the compat shim.
  • LGPD — right to be forgotten. A deletion request targets a Person, cascading to Credentials, Emails, Phones, and Memberships. 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 Membership requires 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 users rows 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: 2 and 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.OrgID must be rewritten. Mitigation: automated refactor pass that replaces Member{OrgID: x} literals with Membership{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_name be mutable? → RESOLVED (2026-06-25): mutable, with audit logging. Immutability is already provided by the Person.id (ULID) anchor — canonical_name is 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; keep preferred_name as the everyday display. (Consistent with kompass-RFC-002's thesis: the anchor is immutable, everything attached is mutable.)
  • Should Email and Phone be separate tables or embedded in Person? → RESOLVED (2026-06-25): separate tables. Verification state and indexability require it. Furthermore, Email is superseded by kompass-RFC-002's user_identities — an email is a user_identities row (kind='email') attached to the Person anchor, carrying verify_methodproofing_levelpurpose/tenant_ctx. Phone stays its own table for now (may fold into user_identities as kind='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-003 can introduce MembershipAssignment or similar. (Renumbered: kompass-RFC-002 is 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_id on the users table for legacy reasons? No — the table itself is replaced by persons + 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-NNN track.

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, vacancies tables exist with the schema above
  • [ ] Koder ID v2 RFC-002 has been amended to reference this RFC
  • [ ] Migration scripts for id-v2 users, Kompass members, c-corp usuario/perfil/permissao, and OrgML are checked in and dry-run tested against CrescerKoderVivver data
  • [ ] New `v1persons