Skip to content

[Architecture/Feature][Organization] 설치형 기업 bootstrap 및 이메일 초대 온보딩 정렬 #641

Description

@HyungminYoon1

배경

Nodease의 공식 제품 방향은 외부 고객 대상 SaaS가 아니라 한 회사가 내부적으로 설치·운영하는 AI Workflow/LLMOps 플랫폼이다. 기본 배포 모델에서는 고객 회사마다 별도 Nodease 배포, PostgreSQL, Redis 및 암호화 키 경계를 사용하고, Organization은 해당 설치 안에서 회사 또는 명시적으로 승인된 최상위 내부 격리 단위, Team은 개발·기획·인사 등 내부 편성 단위로 해석한다.

현재 구현은 모든 신규 가입자에게 개인 기본 Organization과 manager membership, Default Team을 자동 생성한다. 또한 조직원 초대 UI/API는 이미 가입한 사용자의 UUID를 요구하지만, 일반 관리자가 UUID를 검색할 user directory가 없고 이메일 발송도 하지 않는다. 이 흐름은 개인 workspace/SaaS 모델에는 가깝지만 기업 내부 설치형 제품의 bootstrap·온보딩 계약과 맞지 않는다.

목표

  • 설치 시 회사 Organization과 최초 관리자 계정을 한 번만 안전하게 bootstrap한다.
  • 일반 사용자 가입이 개인 관리자 Organization을 자동 생성하지 않도록 foundation 계약을 정렬한다.
  • UUID 직접 입력 대신 이메일 검증 기반 조직 초대·가입·수락 흐름을 제공한다.
  • 가입 전 사용자와 기존 가입자를 모두 지원하고, 수락 후 팀 편성까지 일관되게 처리한다.
  • 물리적 기업별 배포 격리와 애플리케이션의 organization_id 논리 격리 역할을 공식 문서에 명확히 구분한다.

구현 범위

1. 배포 및 Organization 모델 확정

  • 기본 운영 모델을 고객 회사별 독립 배포 + 독립 DB/Redis/keyring으로 문서화한다.
  • Organization을 회사 또는 승인된 최상위 내부 격리 단위로 정의하고 Team과 구분한다.
  • 하나의 설치에서 복수 Organization을 허용할 조건(예: 독립 계열사·사업부)과 비목표를 명시한다.
  • 기존 active Organization scope와 X-Organization-Id 방어는 논리적 격리와 defense-in-depth로 유지한다.

2. 최초 설치 bootstrap

  • 미초기화 설치에서만 실행 가능한 setup/bootstrap 경계를 둔다.
  • 최초 회사 Organization, 최초 active manager membership, Default Team과 관리자 TeamMembership을 하나의 transaction으로 생성한다.
  • 동시 bootstrap 요청에서도 정확히 하나만 성공하도록 DB 제약/lock/idempotency를 적용한다.
  • bootstrap 완료 후 동일 공개 경로를 재사용해 추가 관리자 Organization을 만들 수 없게 한다.
  • bootstrap 실패는 foundation 일부를 남기지 않고 전체 rollback한다.

3. 회원가입 계약 정렬

  • 일반 이메일 회원가입과 최초 OAuth 로그인에서 개인 Organization을 자동 생성하지 않는다.
  • 유효한 조직 초대가 있는 사용자는 검증된 이메일을 기준으로 초대에 연결한다.
  • 초대가 없는 가입자의 허용 상태를 명시적으로 결정한다: 가입 차단, 승인 대기 또는 조직 미소속 제한 계정 중 하나를 공식 계약으로 확정한다.
  • 계정 생성, 초대 수락 및 membership 활성화의 transaction·재시도 경계를 정의한다.

4. 이메일 기반 초대

  • Organization manager가 이메일, 조직 역할(member|manager) 및 선택적 초기 Team을 지정해 초대한다.
  • 이메일은 정규화하되 계정 존재 여부를 외부에 노출하지 않는 동일한 응답 계약을 사용한다.
  • 일회성 opaque invitation token은 원문을 저장·로그·audit하지 않고 verifier hash, 만료, 상태 및 필요한 scope만 저장한다.
  • 초대의 만료, 취소, 재발송, 단일 사용, 재사용 차단 및 반복 요청 멱등성을 구현한다.
  • 기존 가입자는 앱 내부 알림과 이메일을 받고, 미가입자는 초대 링크에서 가입 후 검증된 동일 이메일로 수락한다.
  • 이메일 전송 실패와 membership 생성/활성화를 혼합하지 않도록 durable outbox 또는 명시적인 재전송 가능한 전달 경계를 사용한다.
  • 초대 생성·취소·수락·거절·만료를 안전한 metadata로 audit한다.

5. Team 편성

  • 초대 시 지정한 Team은 동일 Organization의 active Team인지 서버에서 검증한다.
  • TeamMembership은 초대 수락 및 membership 활성화 이후에만 생성한다.
  • 수락 시 membership과 초기 Team 배정을 원자적으로 처리하거나 부분 성공 복구 계약을 둔다.
  • 현재 UI/DB에만 존재하는 is_auto_add를 실제 수락 경로에 연결하거나, 지원하지 않을 경우 UI와 계약에서 제거·대체한다.

6. 관리자·사용자 UI

  • 관리자 초대 화면의 UUID 입력을 이메일, 역할, 초기 Team 선택으로 교체한다.
  • 초대 상태, 만료, 재발송, 취소를 관리할 수 있게 한다.
  • 사용자는 앱 알림 또는 이메일 링크에서 초대를 확인하고 수락·거절할 수 있다.
  • 최초 설치 화면과 일반 로그인/가입 화면을 구분하고 bootstrap 완료 상태를 클라이언트 입력값으로 신뢰하지 않는다.
  • Organization switcher는 실제 복수 Organization membership이 있는 경우에만 의미 있게 표시한다.

7. 기존 데이터·seed·운영 전환

  • 기존 사용자별 개인 Organization을 자동 병합하지 않는다.
  • 기존 설치의 Organization 분류, 관리자 선택, 사용자 membership 및 리소스 이동에 대한 명시적 migration/운영 절차를 마련한다.
  • 리소스·권한·credential·audit 귀속 이동은 별도 승인과 dry-run 결과 없이 수행하지 않는다.
  • demo/test seed를 한 회사 Organization과 내부 Team/사용자 모델로 정렬한다.

보안 및 불변조건

  • 공개 signup/bootstrap 입력으로 Organization ID, manager ID 또는 Team ID를 임의 지정할 수 없어야 한다.
  • invitation token 원문, 이메일 원문 전체, credential 및 raw request body를 로그·trace·audit에 남기지 않는다.
  • 초대 수락 시 token scope, organization, 대상 이메일, 사용자 이메일 검증 상태, 만료·취소·사용 여부를 다시 확인한다.
  • 다른 Organization의 Team이나 role을 초대에 주입할 수 없어야 한다.
  • 마지막 active manager 보호와 manager 권한 변경 정책을 유지한다.
  • 이메일 전송 endpoint에는 rate limit과 abuse 방어를 적용한다.
  • 가입자 존재 여부와 초대 대상 존재 여부를 응답 차이로 열거할 수 없어야 한다.

Acceptance Criteria

  • 빈 설치에서 동시 bootstrap 요청을 보내도 회사 Organization과 최초 manager foundation이 정확히 하나 생성된다.
  • bootstrap 완료 후 일반 가입자는 개인 Organization의 manager가 되지 않는다.
  • 관리자는 사용자 UUID를 알지 못해도 이메일로 조직 초대를 만들 수 있다.
  • 기존 가입자와 미가입자 모두 검증된 초대 경로로 같은 회사 Organization에 합류할 수 있다.
  • 만료·취소·이미 사용·변조된 token과 이메일 불일치 초대는 fail-closed된다.
  • 이메일 전송 실패 후 재시도해도 membership, 알림 또는 이메일이 중복 활성화되지 않는다.
  • 초대에 지정한 초기 Team만 수락 후 배정되며 cross-organization·inactive Team은 거부된다.
  • 초대 수락, 거절, 취소, 재발송 후 Sidebar/Organization switcher 상태가 새로고침 없이 정합하게 갱신된다.
  • 기존 Organization 데이터에 대한 전환은 dry-run과 명시적 관리자 승인 없이는 파괴적으로 실행되지 않는다.
  • 관련 공식 문서, ADR, API/UI 계약 및 정상·실패·동시성·재시도 테스트가 함께 갱신된다.

테스트 범위

  • bootstrap 정상/중복/동시 요청/중간 실패 rollback
  • 초대 없는 signup/OAuth 정책
  • 기존 가입자 및 미가입자 이메일 초대
  • 이메일 대소문자·공백 정규화와 검증되지 않은 이메일
  • token 변조/만료/취소/재사용/동시 수락
  • 초대 재발송과 이메일 provider 실패/중복 delivery
  • 동일 이메일 반복 초대 멱등성 및 suspended/removed membership 재초대
  • cross-organization·inactive Team 주입 및 수락 중 Team 상태 변경
  • 마지막 manager 보호
  • in-app notification/SSE와 Sidebar 조직 목록 동기화
  • 기존 데이터 migration dry-run 및 rollback

비목표

  • 여러 고객 회사를 하나의 공유 DB에서 서비스하는 외부 SaaS 과금·구독 모델
  • 전사 SSO/OIDC/SCIM 전체 구현. 다만 후속 연동을 막지 않는 port와 계약은 유지한다.
  • 승인 없이 기존 Organization과 리소스를 자동 병합하는 작업

관련 이슈

문서 영향

  • docs/PRD.md
  • docs/architecture.md
  • docs/glossary.md
  • docs/features/auth/
  • docs/features/organization/
  • 필요 시 Accepted ADR 신규 작성

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions