Id RFC 012 account models

RFC-012 — Account Models & Domain Strategy

  • *tatus:*Draft
  • *ate:*20260411
  • *uthor:*Koder Team
  • *epends on:*RFC001, RFC002, RFC-006
  • *elated:*RFC013 (invite chain), policies/usernameallocation.kmd

Summary

Defines the account model hierarchy of Koder ID, the domain strategy for user identities, and the phased rollout plan using inviteonly gating. Establishes a singledomain model where every Koder account — employee or external — lives under @koder.dev, with the employee vs. external distinction expressed as identity attributes (roles and cryptographic badges) rather than as separate domains.

Goals

  1. *rand alignment*— the .dev TLD is a symbolic statement of Koder's thesis ("everyone is a dev"); account identities materialize that statement
  2. *ne domain, many layers*— separation of concerns happens via roles, workspaces, and badges — never via splitting identities across multiple domains
  3. *ontrolled rollout*— inviteonly gating lets us build the antiabuse stack and seed the base before public signup
  4. *ulti-tenant from day one*— the same identity can participate in multiple workspaces with different roles
  5. *ortable identity*— users keep their @koder.dev handle when they change workspaces, jobs, or roles

Non-Goals

  • Supporting a separate consumer email domain (e.g., @gmail.com-style)
  • Issuing email addresses from workspace-owned domains (those are provisioned by the workspace admin, not by Koder ID)
  • Social login federation to external IdPs (future RFC)
  • Building Koder Mail itself — this RFC covers identity, not mailbox delivery (Koder Mail has its own roadmap)

Motivation

The email address of a Koder user is not merely a contact handle — it is a *redential of membership in the Koder ecosystem* The product thesis "todos somos dev" implies that becoming a Koder user *s*becoming a dev. If that thesis is real, then the identity must carry it visibly. A splitdomain model where employees get @koder.dev and external users get @koder.me or @koderid.com creates a twoclass system that contradicts the thesis.

The practical concerns that normally block a singledomain model — antiabuse operations, scarce username allocation, reputation risk — are resolved by the inviteonly staged rollout that Koder ID already supports. With invites as the control lever, we can run the singledomain model without incurring day-1 operational debt.

Design

Three account types

Koder ID recognizes three orthogonal entities in the account model. Each entity has a distinct purpose and lifecycle.

1. Individual

A natural person with a single Koder identity. One email, one handle, one password/passkey.

  • *mail:*always <handle>@koder.dev (the only email domain Koder ID issues)
  • *andle:*globally unique, scarce, portable; rules in policies/username-allocation.kmd
  • *wnership:*the individual alone — no workspace can delete or reassign
  • *illing:*personal card, personal plan, B2C apps (Kruze, Koda, Kall personal, Wallet, etc.)
  • *overnance:*none (cannot claim domains, cannot manage members)

An individual exists independently of any workspace. An individual may participate in zero, one, or many workspaces as a member.

2. Workspace

An organization that owns one or more verified internet domains. Workspaces are the unit of B2B account management.

  • *dentity:*a workspace name (slug, humanreadable) plus ≥1 DNSverified domain (crescer.com.br, vivver.com.br)
  • *embers:*users emit workspace-scoped email under the workspace's own domain (ana@crescer.com.br). These are *eparate identities*from the user's Koder individual account, but can be linked in the Koder ID account switcher.
  • *dmin:*workspace admins manage members, groups, SSO, SCIM, policies, and billing
  • *illing:*centralized, seat-based, B2B apps (Flow, Kortex, Verba, Kompass, Talk enterprise, etc.)
  • *overnance:*owns its domains, can create tenants, manages access policies per tenant

One workspace can own N domains (acquisitions, rebranding, multiple brands). The *oder*workspace itself is a regular workspace — it owns koder.dev — and is the workspace that employs the Koder staff. This is dogfooding: Koder uses Koder ID for its own internal identity management.

3. Tenant

A resource-isolated space within a workspace. Tenants are where data lives.

  • *dentity:*a tenant ID scoped to a workspace (crescer.com.br/producao, crescer.com.br/staging)
  • *ata:*KDB rows, Flow repositories, Talk conversations, files, logs — all partitioned by tenant_id
  • *oles:*the same user can have different roles in different tenants of the same workspace (admin in producao, viewer in staging)
  • *illing:*per-tenant quotas and metrics (usage visibility)

Every workspace has at least one default tenant. Small workspaces can ignore tenants entirely — the UX treats a singletenant workspace as if the tenant layer didn't exist. Enterprise workspaces with environment isolation or multiregion needs use the extra layer.

Single-domain strategy

*very Koder identity (Individual) lives under @koder.dev.*No other email domain is issued by Koder ID for individuals.

Consequences:

  • Handles are globally unique across the entire Koder ecosystem — @rodrigo@koder.dev identifies exactly one person, ever
  • Handles are scarce, so allocation is governed by policies/username-allocation.kmd
  • Employee/external separation is *ot*expressed in the email domain; it is expressed in role attributes and cryptographic badges (see below)
  • Workspace members whose email is ana@crescer.com.br are *eparate identities*— those emails are issued by the workspace domain, not by Koder ID. A person can link an individual ana@koder.dev and a workspace ana@crescer.com.br in their account switcher.

Why not split (rejected alternative)

We considered splitting into corporate and consumer domains — @koder.dev for staff and @koder.me / @koderid.com for external users. Rejected because:

  1. *rand violation*— creates a two-class system ("staff with the good domain" vs. "customers with the other domain") that contradicts "todos somos dev"
  2. *cTLD risk*— .me is a repurposed Montenegro ccTLD with political and jurisdictional exposure that .com/.dev do not have
  3. *perational complexity*— two email operations to run instead of one; users have to pick which domain to sign up under; account switching across domains is awkward
  4. *oder.com is unavailable*— the obvious split target (@koder.com like @gmail.com) is registered and not available for purchase at reasonable cost

Why not subdomain (rejected alternative)

We considered @staff.koder.dev for employees and @koder.dev for externals. Rejected because:

  1. Subdomains in email are unusual and create suspicion ("is this a real email?")
  2. Still creates a two-class system, just with a smaller visible gap
  3. Breaks the "your email is the same as your handle globally" property — handles and emails diverge

Employee vs external separation

With a single domain, the distinction between Koder employees and external users is carried by *dentity attributes* not by the email address.

Roles

The Koder workspace (the one that owns koder.dev) assigns roles to members of the workspace. Roles include:

  • staff — current Koder employee
  • contractor — current external contractor with time-bound engagement
  • alumni — former Koder employee or contractor
  • intern — current intern
  • board — board member or investor
  • (no role) — external user with no Koder employment relationship

A role is a property of the user's *embership in the Koder workspace* not of the identity itself. When the person leaves Koder, the role changes from staff to alumni; the @rodrigo@koder.dev identity persists unchanged.

Cryptographic badge

Staff status is visually marked by a *ryptographic badge*displayed next to the user's handle in any Koder UI (Kruze, Kall, Talk, Kortex, Flow, Store, etc.).

  • The badge is signed by a Koder-controlled key (stored in the Koder workspace's HSM or sealed secret store)
  • The signature covers {handle, role, issued_at, expires_at, workspace_id} and is verifiable offline by any client
  • Badge expiry is ≤30 days; renewal is automatic while the role is still active
  • Revocation list is published by the Koder ID admin service and cached by clients

A badge is the *nly*trustworthy signal that a user represents Koder. Clients must never infer "this user is staff" from the email domain alone — the email domain only tells you the user has a Koder identity, not that they work for Koder. This is the crucial invariant that prevents the single-domain model from becoming a phishing vector.

Other badges

The same badge machinery is reused for other verification classes (each with its own issuer key):

  • verified — human identity verified via KYC (personal)
  • org-admin — admin of at least one workspace (contextual)
  • invited-by-staff — identity issued via a Koder staff invite (trust signal, see RFC-013)
  • developer — has published ≥1 app on the Koder Hub / contributed to a Koder open-source project

Badges are additive, not exclusive. A user can hold multiple badges simultaneously.

Rollout phases

The singledomain model relies on inviteonly gating for its first two phases. RFC-013 specifies the invite machinery; this section describes the phase boundaries and the conditions for transition.

Phase 0 — Internal alpha (closed)

  • *ligibility:*Koder staff only, provisioned manually by the Koder ID admin
  • *oal:*dogfood the full identity stack (Koder ID + Koder Mail + account switcher + badges) on a small, trusted base
  • *sername allocation:*staff pick their preferred handle; 2letter handles (@ro, @an) and firstname handles (@rodrigo, @ana) are allocated during this phase
  • *xit criteria:*
    • Identity service in production on KDB with all tests passing
    • Koder Mail delivers staff↔staff email with DMARCSPFDKIM passing
    • Account switcher works across staff identities + ≥1 workspace membership
    • Badge signing and verification working in ≥1 Koder client (starts with Kruze)

Phase 1 — Invite-only seed (external)

  • *ligibility:*invite-only; each staff member gets an initial pool of N invites (default N=10, configurable)
  • *oal:*populate the base with curated external users, detect abuse patterns at small scale, build reputation for koder.dev as a sending domain
  • *sername allocation:*firstcome, subject to the reservation policy (`policies/usernameallocation.kmd`); no overrides — invited users pick from what is still available
  • *nvite chain:*every invite is tracked in the invitechain tables (RFC013) with full lineage — we know exactly who invited whom, when, and why
  • *rust score:*each identity has a derived trust score based on distance from staff in the invite chain; used by anti-abuse and by UI ranking in StoreTalkKall
  • *xit criteria:*
    • ≥10,000 active external identities
    • Antiabuse pipeline detects and mitigates ≥95% of simulated abuse in redteam drills
    • Outbound reputation (Google Postmaster Tools, Microsoft SNDS) in the green for 30 consecutive days
    • Support pipeline handles identity disputes (trademark, impersonation) within agreed SLAs

Phase 2 — Public signup

  • *ligibility:*anyone with a verifiable email + phone + captcha
  • *llocation:*first-come for available handles; most valuable handles already allocated during phases 0 and 1
  • *riction:*email verification, phone verification, captcha, rate limits, fraud detection — all active
  • *nvite chain:*signup via invite is still preferred but no longer required; direct-signup users have an empty invite lineage and a lower initial trust score
  • *o reversal:*once phase 2 is entered, we do not go back to invite-only

Transitions between phases are *neway* Once Phase 1 opens, Phase 0 closes (no more direct staff provisioning — staff join via the same invite flow as everyone else, just with the staff role preassigned). Once Phase 2 opens, Phase 1 closes.

Invariants

  1. *very individual email is <handle>@koder.dev.*No other domain is ever issued by Koder ID for individuals.
  2. * handle is globally unique, forever.*Once allocated, a handle cannot be reassigned to a different person — even after the original user deletes their account. See policies/username-allocation.kmd for the preservation/release rules.
  3. *ole ≠ identity.*A user's role in the Koder workspace can change freely; the identity persists.
  4. *adge is the only authoritative source of staff status.*No client may infer staff status from the email domain or any other field.
  5. *orkspace membership is optional.*An individual can exist with zero workspace memberships (personal account only).
  6. *enant membership is optional.*A workspace member does not automatically gain access to all tenants in that workspace — each tenant has its own role bindings.
  7. *nvite lineage is immutable.*Once recorded, an invite edge is never deleted or mutated — only superseded by new events (revocation, abandonment). See RFC-013.

Data model (summary)

Full schema in RFC-002 (updated in parallel). Key tables for this RFC:

-- koder_id_identity namespace
CREATE TABLE users (
    id              TEXT PRIMARY KEY,
    tenant_id       TEXT NOT NULL,  -- always the Koder ID system tenant for individuals
    handle          TEXT NOT NULL UNIQUE,  -- the part before @koder.dev
    email           TEXT NOT NULL UNIQUE,  -- always <handle>@koder.dev
    email_verified  BOOLEAN NOT NULL DEFAULT FALSE,
    display_name    TEXT,
    avatar_url      TEXT,
    locale          TEXT DEFAULT 'en-US',
    status          TEXT NOT NULL DEFAULT 'active',
    trust_score     INTEGER NOT NULL DEFAULT 0,  -- derived, see RFC-013
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at      TIMESTAMPTZ
);

-- koder_id_workspace namespace
CREATE TABLE workspaces (
    id              TEXT PRIMARY KEY,
    slug            TEXT NOT NULL UNIQUE,
    name            TEXT NOT NULL,
    plan            TEXT NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    ...
);

CREATE TABLE workspace_domains (
    workspace_id    TEXT NOT NULL REFERENCES workspaces(id),
    domain          TEXT NOT NULL UNIQUE,
    verified_at     TIMESTAMPTZ,
    verification_method TEXT,  -- dns_txt, http, etc
    PRIMARY KEY (workspace_id, domain)
);

CREATE TABLE workspace_members (
    workspace_id    TEXT NOT NULL REFERENCES workspaces(id),
    user_id         TEXT NOT NULL REFERENCES users(id),
    role            TEXT NOT NULL,  -- staff, contractor, alumni, intern, board, external
    joined_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
    left_at         TIMESTAMPTZ,
    PRIMARY KEY (workspace_id, user_id)
);

CREATE TABLE tenants (
    id              TEXT PRIMARY KEY,
    workspace_id    TEXT NOT NULL REFERENCES workspaces(id),
    slug            TEXT NOT NULL,
    name            TEXT NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (workspace_id, slug)
);

CREATE TABLE tenant_role_bindings (
    tenant_id       TEXT NOT NULL REFERENCES tenants(id),
    user_id         TEXT NOT NULL REFERENCES users(id),
    role            TEXT NOT NULL,  -- owner, admin, editor, viewer, (custom)
    granted_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    revoked_at      TIMESTAMPTZ,
    PRIMARY KEY (tenant_id, user_id)
);

Invitechain tables are defined in RFC013.

API sketch

New endpoints on the Identity Service:

POST   /v1/individuals                     # create individual (invite-only in phase 0/1)
GET    /v1/individuals/me                  # return current identity with roles and badges
GET    /v1/individuals/by-handle/:handle   # public lookup (handle → display name, badges)
PATCH  /v1/individuals/me                  # update display name, locale, avatar
DELETE /v1/individuals/me                  # soft-delete (handle preserved, see policy)

POST   /v1/workspaces                      # create workspace (requires trust score above threshold)
POST   /v1/workspaces/:id/domains          # claim a domain (returns DNS TXT challenge)
POST   /v1/workspaces/:id/domains/:domain/verify
POST   /v1/workspaces/:id/members          # add member (by handle)
POST   /v1/workspaces/:id/tenants          # create tenant
POST   /v1/tenants/:id/role-bindings       # grant role

GET    /v1/badges/:user_id                 # fetch badges (cached, short TTL)
POST   /v1/badges/verify                   # verify a badge signature offline

Security considerations

  1. *hishing via fake staff badges*— mitigated by cryptographic signing, short expiry, published revocation list, and client-side verification
  2. *andle squatting*— mitigated by policies/username-allocation.kmd reservation list + invite gating
  3. *rademark disputes*— handled by a formal reclamation process documented in the username policy
  4. *omain reputation poisoning*— mitigated by outbound rate limits per trust score, anomaly detection on outbound content, and ability to suspend individual users without affecting the domain
  5. *ccount takeover*— mitigated by WebAuthn/passkeyfirst auth (RFC003), TOTP fallback, recovery codes; badge signature does not attest "this session is the real user" — it attests "this handle has this role"
  6. *orkspace impersonation*— mitigated by DNS TXT verification for all claimed domains and by disallowing koder.dev subdomains as verifiable workspace domains

Alternatives considered

Alternative Rejected because
Split domain: @koder.dev for staff + @koder.me for external ccTLD risk, two-class system, brand violation
Split domain: @koder.dev for staff + @koderid.com for external Two-class system, extra domain to operate, brand dilution
Split domain: @koder.dev for staff + @koder.com for external koder.com unavailable; would cost US$ 5k–50k on aftermarket
Subdomain: @staff.koder.dev for staff Email subdomains feel unusual, still two-class
No separation: everyone is @koder.dev with no visible marker Unsafe — trivial phishing vector, no way to signal staff
Everyone brings their own email (StripeLinearNotion model) Loses the brand thesis entirely; Koder becomes just another B2B SaaS

Decisions — hyperscalealigned (202604-11)

The items below were formerly open questions and are now *inding decisions* resolved in favor of the 100M+ tenant hyperscale target (see project_kdb_next and kdb + id v2 — methodical memories). Items that require empirical calibration remain open in the section that follows, but their shape is frozen.

Handle length and tiers

Tier Length Rule
*1* 1 char *ermanently nonallocable*— never issued to any identity, including staff. Rationale: singlechar handles fail confusability checks trivially, create enumeration attack surface, and have negligible UX value. All 36 possible T1 handles (az, 09) are locked in the reservation dictionary forever.
*2* 2 chars Staff-only forever. Allocated during Phase 0.
*3* 3 chars Gated by trust score ≥800 in Phases 0/1. Opens to first-come in Phase 2 if still available.
*4+* 4–30 chars Firstcome, subject to reservation dictionary. *aximum is 30*(not 20) — at hyperscale, real names with separators (joao.almeida.santos, `mariaclara-rezende`) need the room.

Alumni policy

When a staff member leaves Koder, the workspace_members row transitions *mmediately*from role = staff to role = alumni. No grace period — grace periods are attack surface where the compromised/disgruntled account can still sign documents, issue invites, or perform staff-marked actions.

  • *andle retention:*permanent. Alumni keep @rodrigo@koder.dev forever, including the handle slot in the reservation dictionary (the handle is never released, even if the person deletes the account later).
  • *adge transition:*the staff badge (cryptographically signed, green marker in UI) rotates out of the badge signing chain within the badge's normal TTL (≤30 days). During that window, the UI shows a transitional label "Koder Staff (ending)". After expiration, the user holds a new alumni badge (cryptographically signed, neutral marker, no color).
  • *pt-out:*an alumnus can request the alumni badge be removed entirely for anonymity. Handle reservation is not affected.

Workspace guest accounts

A new first-class role guest is introduced in workspace_members:

  • *cope:*bound to specific tenants and specific resources within a workspace, not the full workspace namespace. A guest in crescer.com.br/producao does not automatically see crescer.com.br/staging.
  • *illing:*does *ot*consume a seat. Guests are free up to a per-workspace cap (default 50; configurable by admin).
  • *xpiration:*auto-revoked after *0 days*of idleness on the workspace (last access to any resource in the workspace). Workspace admin can explicitly extend or convert to full member.
  • *dentity:*guests retain their individual Koder identity — there is no separate "guest account" object. The @rodrigo@koder.dev in a guest role is the same @rodrigo@koder.dev in their personal context.
  • *nvite propagation:*guests cannot invite other guests into the workspace. Breaking this rule would allow an external inviter to seed an entire guest subgraph inside a workspace bypassing the admin's curation.
  • *ap on guest membership per user:*none. A user can be a guest in N workspaces simultaneously.

Handle changes

A user may change their handle *nce per rolling 365 days*

  1. New handle is allocated through the standard flow (validated against format rules and reservation dictionary)
  2. Old handle enters pending_release state *mmediately* but with an *liasing window of 30 days*— during which the old handle still routes email, links, and handle lookups to the user's new identity. This prevents broken bookmarks and outofdate references from silently failing.
  3. After the 30-day window, the old handle is frozen in the reservation dictionary as "formerly allocated to user id" — *ot released to the pool, not reclaimable by any other user, ever*
  4. The 365day cooldown starts from the change date. Frequent changers (users approaching the 1per-year limit) see a UI warning before the change is confirmed.

This balances legitimate name changes (marriage, gender transition, typo fixes) with the anti-squatting goal of handle permanence. Frozen historical handles never reappear, which eliminates a class of impersonation attacks.

Trust score storage and bounds

  • *ype:*INTEGER in schema but constrained to [0, 10000] at the service layer. A 14-bit effective range is enough resolution for all decision thresholds in this design and avoids the complexity of floats.
  • *ecomputation:*nightly batch job in KDB, partitioned by the system tenant and sharded by handle-prefix (26 shards for az + 1 for digits + 1 for specials). The nightly recomputation at 10⁸ identities is sized to complete in ≤30 minutes on the target KDB cluster.
  • *ot-path reads:*never. When a client asks "can I issue invites?", the answer comes from the denormalized invite_quotas.period_allowed column (updated eagerly on score changes), not from recomputing the score on each request. This is a hyperscale invariant: scores are read from cache, never computed inline.

Regional partitioning and global handle uniqueness

At ≥10⁸ identities, the users table is partitioned by hash(lower(handle)) across regions (initial deployment: 3 regions, scalable to 16). This distributes write load evenly and localizes reads for the majority of each region's population.

Global handle uniqueness — the invariant that no two identities worldwide share a handle — is enforced via a dedicated handle_registry table:

  • *ppend-only*— rows are only inserted (never updated, never deleted)
  • *trong consistency*on insertion using KDB's CAS primitives
  • *inglewriter pattern per letter prefix*— 36 writers (one per az, one per 09) elected via KDB leader election, plus a fallback writer for multibyte edge cases. This pattern gives O(1) allocation throughput per region while preserving global uniqueness — no global lock, no hot spot.
  • *chema added in the RFC-002 amendment*(to be written as part of this decision)

Handle allocation flow at hyperscale: candidate handle → hash to prefix → route write to prefix's elected leader → CAS insert → return success/conflict → on success, also insert into regionlocal users table. Twophase but bounded by a single crossregion roundtrip per allocation.

Email domain reputation at scale

With 10⁸ senders under a single koder.dev domain, reputation becomes a shared resource. These mitigations are binding on Koder Mail:

  1. *iered outbound IP pools:*users send through different IP ranges based on trust score. Highscore users share the bestreputation pool. Lowscore users start in a warmup pool. Middle tier uses midreputation pools. This isolates spam contagion: a compromised lowtrust account cannot poison the highreputation pool.
  2. *er-user hard rate limits*that scale with trust score (e.g., 50 outboundday for score <300, 500day for 300–699, 5000/day for 700+)
  3. *utbound content classification*— every outgoing message passes a lightweight ML classifier (fine-tuned on open datasets, not a whole LLM) that flags phishing patterns and throttles in real time
  4. *eputation bonding:*accounts in their first 30 days cannot send outside @koder.dev (they can still send in-network). This starves spammers of the main outbound target.
  5. *ascade suspension on abuse:*when the invitechain cascade revocation (RFC013) flags a subtree, all affected users are instantly moved to the lowestreputation pool until reevaluated

These are not optional infrastructure — they are a *recondition*for the singledomain strategy to work at hyperscale. If Koder Mail cannot provide them, this RFC's model needs to be reevaluated.

Remaining open questions

Items that require empirical calibration during Phase 0/1 or UX input. Their shape is frozen; only numerical constants remain to be tuned.

  1. *rust score formula constants*— the structure (linear decay by depth, capped adjustments, contagion penalty) is locked. The specific coefficients (-50 * depth, +20 per invited, cap at +200, etc.) need empirical calibration against Phase 0 data before Phase 1 opens.
  2. *uest cap default*— current default is 50 per workspace; may need adjustment based on early workspace feedback
  3. *andlechange 365day cooldown anchor*— rolling vs. fixed anniversary. Current decision: rolling. Revisit if users find it confusing.
  4. *lumni badge visibility default*— optin (show the badge only if the alumnus wants it) vs. optout (show by default, let them hide). Lean toward opt-out pending legal review.

Out of scope (for this RFC, covered elsewhere)

  • *nvite mechanics*→ RFC-013
  • *sername allocation rules*→ meta/docs/stack/policies/username-allocation.kmd
  • *uthentication protocols (OIDCOAuth2WebAuthn)*→ RFC003, RFC004
  • *ession management*→ RFC-005
  • *torage schema details*→ RFC002, RFC011

Changelog

  • 20260411 — Initial draft

Source: ../home/koder/dev/koder/meta/docs/stack/rfcs/id-RFC-012-account-models.md