engineering docs

STUDI KASUS

Navigasi dokumen: gunakan 03.overview untuk boundary dan ownership arsitektur, 02.roadmap untuk status/urutan implementasi, dan AGENTS.md pada root repository sebagai aturan eksekusi serta pembaruan dokumen.

Misalnya ada bisnis:

Organization:
Satoe Coffee Indonesia

memiliki:

  • 3 cabang
  • 25 karyawan
  • payroll bulanan
  • attendance GPS
  • inventory
  • shift
  • HR manager
  • supervisor
  • kasir

STEP 1 — USER REGISTER


User membuat akun

User:
- email
- password
- oauth
- MFA

Database

User
 ā”œā”€ā”€ id
 ā”œā”€ā”€ email
 ā”œā”€ā”€ password
 └── platformRole

Penting

User belum punya:

  • salary
  • attendance
  • payroll
  • jabatan

Karena:

User hanya identity.

Readiness identity

Pada tahap ini sistem hanya boleh menampilkan UserProfileReadiness: kelengkapan profil personal yang tidak membutuhkan Organization, Member, Employee, Team, atau Workspace. Jika Ahmad kemudian menjadi owner Satoe Coffee dan Sarah menjadi Member/Employee, masing-masing memperoleh projection OrganizationReadiness dan MemberWorkReadiness yang terpisah. Dengan demikian pegawai Pemkab pada studi kasus 17 Agustus tidak pernah memperoleh atau dipengaruhi readiness Organization A hanya karena ia menjadi participant collaboration terbatas.


STEP 2 — USER MEMBUAT ORGANIZATION


Ahmad membuat Organization

Organization:
Satoe Coffee Indonesia

Database

Organization
 ā”œā”€ā”€ id
 ā”œā”€ā”€ name
 ā”œā”€ā”€ ownerId
 └── etc

Relasi

User (Ahmad)
 └── owns Organization (ownedOrganizations)

STEP 3 — OWNER MENJADI MEMBER

Ketika Organization dibuat:

sistem otomatis membuat:

Member

Kenapa?

Karena:

Hanya member organisasi yang bisa mengakses organisasi tersebut, owner juga perlu akses ke Organization.


Database

Member
 ā”œā”€ā”€ userId
 ā”œā”€ā”€ organizationId
 ā”œā”€ā”€ permissionRoleId
 └── status

STEP 4 — ORGANIZATION MEMBUAT PERMISSION ROLE

Ahmad membuat role:

owner
hr_manager
supervisor
cashier
warehouse_admin

Database

PermissionRole
 ā”œā”€ā”€ name
 ā”œā”€ā”€ permissions[]
 └── organizationId

Contoh

cashier

punya permission:

order.create
order.view
attendance.checkin

hr_manager

punya permission:

employee.create
payroll.generate
payroll.approve
attendance.approve

STEP 5 — ORGANIZATION MEMBUAT POSITION


Ahmad membuat struktur jabatan

CEO
Branch Manager
Supervisor
Barista
Cashier
Kitchen Staff

Database

Position
 ā”œā”€ā”€ name
 ā”œā”€ā”€ parentId
 └── organizationId

Hierarchy

CEO
 └── Branch Manager
      └── Supervisor
           ā”œā”€ā”€ Cashier
           └── Barista

Kenapa Position dipisah dari Permission Role?

Karena:

Position:
Cashier
 
Permission Role:
pos_operator

atau:

Position:
Branch Manager
 
Permission Role:
limited_manager

STEP 6 — OWNER MENGUNDANG USER


Ahmad mengundang Sarah

email:
sarah@gmail.com

Database

Invitation
 ā”œā”€ā”€ email
 ā”œā”€ā”€ token
 ā”œā”€ā”€ target="member" ("user"|"member"|"employee")
 ā”œā”€ā”€ inviterId
 ā”œā”€ā”€ inviteeId
 └── organizationId (ketika undangan member untuk workspace menggunakan workspaceId)
 
Member
 ā”œā”€ā”€ userId
 ā”œā”€ā”€ organizationId
 ā”œā”€ā”€ permissionRoleId
 └── etc

Sarah menerima undangan

Sekarang:

Sarah
 └── menjadi Member (Organization)

STEP 7 — SARAH MENJADI EMPLOYEE


Penting

Tidak semua member adalah employee.

Tetapi Sarah adalah staff.

Maka dibuat:

Employee

Database

Employee
 ā”œā”€ā”€ userId
 ā”œā”€ā”€ organizationIdId
 ā”œā”€ā”€ memberId
 ā”œā”€ā”€ employeeCode
 ā”œā”€ā”€ positionId
 ā”œā”€ā”€ type
 ā”œā”€ā”€ status
 └── etc

Kenapa Employee dipisah?

Karena sekarang Sarah punya:

  • payroll
  • BPJS
  • kontrak
  • attendance
  • shift
  • salary

STEP 8 — POSITION HISTORY


Sarah awalnya Cashier

Cashier

Database

EmployeePositionHistory
 ā”œā”€ā”€ employeeId
 ā”œā”€ā”€ newPositionId
 ā”œā”€ā”€ effectiveFrom
 └── changedById

1 tahun kemudian

Sarah dipromosikan:

Supervisor

History baru dibuat

Cashier -> Supervisor

Kenapa ini penting?

Karena payroll lama:

  • harus tetap tahu Sarah dulu cashier
  • attendance approval lama tetap valid
  • laporan historis tetap akurat

STEP 9 — ROLE HISTORY


Awalnya permission Sarah:

cashier_role

Setelah promosi

menjadi:

supervisor_role

Database

RoleHistory
 ā”œā”€ā”€ oldRoleId
 ā”œā”€ā”€ newRoleId
 ā”œā”€ā”€ changedById
 └── effectiveAt

Kenapa PositionHistory dipisah dari RoleHistory?

Karena:

Position:
Cashier -> Supervisor
 
tetapi
 
Permission:
pos_operator -> limited_manager

dan itu domain berbeda.


MEMBUAT WORKSPACE

Dalam keputusan internal Owner menunjuk sarah sebagai kepala tim dalam agenda tertentu.

Sarah membuat:

Workspace
  → Mengundang anggota beberapa lainnya.
    → etc

Sehingga setiap anggota workspace sudah pasti Member Organization, namun tidak semua member Organization adalah anggota workspace tersebut.


STEP 10 — ATTENDANCE


Sarah check-in

mobile app mengirim:

clockIn
GPS
selfie
device
IP

Database

Attendance
 ā”œā”€ā”€ employeeId
 ā”œā”€ā”€ clockIn*
 ā”œā”€ā”€ clockOut*
 ā”œā”€ā”€ clockInLocation
 ā”œā”€ā”€ lateMinutes
 ā”œā”€ā”€ overtimeMinutes
 └── approvedById

GeoPoint

GeoPoint
 ā”œā”€ā”€ latitude
 ā”œā”€ā”€ longitude
 ā”œā”€ā”€ accuracy
 └── device

Kenapa GeoPoint penting?

Karena:

  • anti fake GPS
  • geofence
  • audit lokasi
  • field tracking

STEP 11 — PAYROLL CONFIGURATION


Sarah punya:

basic salary
meal allowance
BPJS
tax
overtime

Database

EmployeeCompensationComponent
 ā”œā”€ā”€ componentType
 ā”œā”€ā”€ category
 ā”œā”€ā”€ amount
 └── rate

Contoh

BASIC_SALARY
5.000.000

BPJS_HEALTH
1%

STEP 12 — PAYROLL GENERATION


Sistem generate payroll bulan Mei


Payroll dibuat

Payroll
 ā”œā”€ā”€ grossSalary
 ā”œā”€ā”€ totalDeduction
 ā”œā”€ā”€ netSalary
 ā”œā”€ā”€ status
 └── period

PayrollItem dibuat

Basic Salary
Overtime
Meal Allowance
BPJS
Tax
Late Deduction

Kenapa Payroll immutable?

Karena:

payroll bulan lalu tidak boleh berubah.

Walaupun:

  • salary sekarang berubah
  • position berubah
  • BPJS berubah

STEP 13 — INVENTORY DAN COMMERCE LINTAS DATABASE

STEP 13 bukan transaksi dalam satu database global. Organization adalah tenant root di Runner; catalog, commerce, dan inventory menyimpan organizationId sebagai tenant key terverifikasi oleh API, bukan foreign key lintas database.

Runner DB:     Organization, Member, Team, Workspace, Collaboration, Employee, PermissionRole
Billing DB:    BillingAccount, Plan, Subscription, Entitlement, UsageRecord
Catalog DB:    Product, Variant, Price, Category
Commerce DB:   Customer, Order, OrderItem, Payment, Shipment
Inventory DB:  Warehouse, Stock, InventoryMovement, Reservation

Sebuah workspace boleh dipakai sebagai konteks operasional pada dokumen domain bila modul membutuhkannya, tetapi tidak menggantikan organizationId. Semua query selalu di-scope oleh organization terlebih dahulu.

13A — Penerimaan barang

Gudang menerima 50 kg kopi. Inventory API memvalidasi actor dari session, memastikan actor adalah member aktif tenant, lalu membuat ledger movement yang idempoten memakai reference/event key.

InventoryMovement
ā”œā”€ā”€ organizationId
ā”œā”€ā”€ warehouseId
ā”œā”€ā”€ productId / variantId (referensi Catalog)
ā”œā”€ā”€ type = IN
ā”œā”€ā”€ quantity = 50
ā”œā”€ā”€ referenceType = GOODS_RECEIPT
ā”œā”€ā”€ referenceId
└── idempotencyKey

WarehouseStock adalah proyeksi/materialized balance yang diperbarui bersama movement di Inventory DB. Ia bukan sumber audit; sumber kebenarannya tetap ledger movement.

13B — Penjualan dan pengurangan stok

Order dibuat di Commerce DB setelah Commerce API memvalidasi tenant dan harga/snapshot dari Catalog. Ketika order mencapai state yang mengonsumsi persediaan, Commerce menerbitkan event/outbox; Inventory memproses event itu secara idempoten dan membuat OUT atau RESERVE movement. Tidak ada transaksi Prisma lintas Runner/Catalog/Commerce/Inventory.

Kegagalan antar layanan ditangani dengan outbox, retry terukur, dan rekonsiliasi; bukan dengan mengubah saldo stok dari UI atau menganggap request browser sebagai sumber kebenaran.

STEP 14 — EMPLOYEE RESIGN, AKSES, DAN WORKSPACE

Ketika Sarah resign, tiga lifecycle dievaluasi secara terpisah:

User              tetap ada (identity dan audit)
Member            dapat tetap aktif, disuspensi, atau direvoke sesuai keputusan akses
WorkspaceParticipant direvoke hanya untuk workspace yang tidak lagi diikuti
Employee           employmentStatus = RESIGNED

Resign tidak otomatis menghapus Member. Seorang mantan karyawan mungkin tetap memiliki akses non-employment yang sah, misalnya sebagai owner, auditor, atau collaborator. Sebaliknya, revoke Member harus mencabut seluruh assignment WorkspaceParticipant aktif dan seluruh permission organisasi; Employee dan sejarah payroll tetap dipertahankan.

14A — Konsistensi domain

  • Runner DB menyimpan perubahan Member, WorkspaceParticipant, Employee, PositionHistory, dan RoleHistory dalam boundary domain Runner.
  • Payroll yang sudah finalized tidak diubah. Koreksi dilakukan melalui adjustment/reversal baru sesuai domain payroll.
  • Commerce dan Inventory tidak menghapus order atau movement historis ketika employee/member direvoke; referensi actor disimpan sebagai audit snapshot/ID yang aman.

14B — Rehire

Saat Sarah kembali, sistem tidak membuat ulang User. HR mengaktifkan atau membuat employment record sesuai kebijakan, mencatat posisi efektif baru, lalu owner/authorized admin memberikan Member role dan assignment workspace yang diperlukan. Riwayat lama tetap immutable.

STEP 15 — CATEGORY SEBAGAI KONFIGURASI, BUKAN OTORISASI

Organization.category membatasi vocabulary, template, dan urutan modul yang relevan (misalnya retail menonjolkan catalog/commerce/inventory; service menonjolkan booking). Category bukan role, permission, workspace, atau entitlement. Ia tidak memberi akses dan tidak boleh menjadi satu-satunya dasar API authorization.

Host container hanya boleh menampilkan shortcut category apabila route/micro-frontend telah benar-benar terpasang dan permission API telah tersedia. Category tidak boleh menghasilkan URL palsu untuk product, order, inventory, atau analytics.

STEP 16 — SUBSCRIPTION, ENTITLEMENT, DAN RETENSI

Billing adalah control-plane terpisah, bukan bagian dari Runner ataupun Commerce. Satu BillingAccount awalnya merepresentasikan satu Organization melalui organizationId scalar yang diverifikasi ke Runner. Invoice Billing adalah tagihan Aori kepada Organization; ia bukan Invoice Commerce antara Organization dan customer.

Billing DB
ā”œā”€ā”€ BillingAccount       identitas penagihan tenant
ā”œā”€ā”€ Plan                 versi produk/komersial Aori
ā”œā”€ā”€ Subscription         lifecycle subscription dan snapshot plan
ā”œā”€ā”€ Entitlement          feature/quota efektif tenant
ā”œā”€ā”€ UsageRecord          event metering immutable dan idempoten
ā”œā”€ā”€ BillingInvoice       tagihan Aori → Organization
└── BillingPayment       pembayaran tagihan Aori

Setiap mutasi berfitur memeriksa dua boundary secara terpisah:

1. Entitlement: tenant berhak memakai feature dan belum melewati limit?
2. Authorization: actor memiliki permission untuk melakukan aksi?

Satu boundary tidak menggantikan yang lain. Plan tidak memberi user hak untuk mengundang partner, dan permission tidak dapat melampaui quota plan. Domain yang memiliki resource menghitung usage secara atomik dan kemudian mengirim UsageRecord dengan idempotency key; Billing tidak boleh menjadi dependency write sinkron untuk seluruh domain operasional.

Ketika subscription berhenti, transisi minimalnya adalah active → grace_period → restricted_read_only (policy application) → archived → purged. Data tidak langsung dihapus: data aktif dipertahankan selama periode pembukuan, kemudian diarsipkan, dan hanya dipurge jika retention policy/legal terpenuhi setelah pemberitahuan serta kesempatan export backup kepada Organization.

STEP 17 — KOLABORASI ANTAR-ORGANIZATION YANG DAPAT DIAUDIT

Misal: User X membuat Organisasi A dan melengkapi seluruh entity lainnya (seperti: permissionRole, dll), User X mengundang Member baru sebagai HR, HR otomatis menjadi Member dan karyawan (karena harus ada payroll), HR merekrut karyawan baru, HR mengelompokkan beberapa karyawan dalam satu Team misal Team bernama T1. Team T1 memiliki beberapa anggota (semua anggota Team sudah pasti dan harus merupakan Member Organization dan Employee), HR menugaskan beberapa Member/Employee untuk membuat sebuah agenda acara ulang tahun perusahaan (dapat lintas Team), Member/Employee yang ditugaskan tersebut akan di kumpulkan didalam satu workspace (contoh kasus workspace internal Organisasi). Kemudian suatu saat ada agenda 17 Agustusan, HR menugaskan beberapa Member/Employee untuk megurus agenda tersebut, maka akan dikumpulkan lagi didalam satu workspace baru yang berbeda, ternyata acara 17 Agustusan diadakan di halaman pemkab, maka organisasi A harus menerbitkan surat tugas dan surat kerjasama, untuk prosesi pembahasan yang transparan maka pegawai pemkab akan diundang untuk masuk kedalam workspace baru tersebut.

Kasus acara 17 Agustus menunjukkan bahwa workspace bukan organisasi virtual dan participant eksternal tidak boleh dipindahkan ke tenant host. Organization A memiliki workspace Panitia 17 Agustus; Pemkab tetap organization mandiri. Pegawai Pemkab menjadi Member Pemkab, lalu diberi scope kolaborasi terbatas.

Organization A (owner sah Workspace)
└── Workspace: Panitia 17 Agustus
    ā”œā”€ā”€ participant internal: Member/Employee A dari beberapa Team
    └── participant eksternal: Member Pemkab, scope terbatas
 
Collaboration: Panitia 17 Agustus
ā”œā”€ā”€ Party: Organization A       role=owner
ā”œā”€ā”€ Party: Pemkab               role=participant
ā”œā”€ā”€ Party: Sponsor B            role=participant
└── Party: Team HR A            role=approver/internal participant

Project menyimpan target, timeline, budget, deliverable, dan status. Collaboration menyimpan legal basis, party, approval, scope, masa berlaku, dan dokumen. Workspace adalah ruang operasional agenda. Ketiganya bukan sinonim: satu Project dapat memiliki banyak Collaboration/Workspace dan satu Collaboration dapat mengatur banyak Workspace milik organization owner.

CollaborationParty
ā”œā”€ā”€ organizationId wajib; teamId opsional
ā”œā”€ā”€ role: owner | participant | observer | approver
ā”œā”€ā”€ status: invited | accepted | suspended | revoked
ā”œā”€ā”€ grantedScopes dan masa berlaku
└── representedByMemberId / assignmentDocumentId bila relevan
 
CollaborationApproval
ā”œā”€ā”€ requiredByRole / requiredByTeamRole
ā”œā”€ā”€ party yang harus menyetujui
└── approvedByMemberId, status, decidedAt, note
 
CollaborationDocument
ā”œā”€ā”€ agreement | assignment_letter | nda | minutes | attachment
└── issuerOrganizationId, document reference opaque, validity, verification

Urutan aman participant eksternal

  1. Member internal berpermission membuat draft Collaboration dan party owner.
  2. API memverifikasi member aktif, organization owner, serta Team bila ada.
  3. Party eksternal dibuat invited; approval wajib dibuat menurut policy.
  4. Partner menerima dengan Member aktif dari organization party yang sama.
  5. Workspace hanya dihubungkan setelah approval wajib selesai dan semua validity window aktif.
  6. External WorkspaceParticipant wajib menunjuk party aktif; scope-nya subset party dan tidak boleh melampaui masa berlaku party/collaboration.
  7. Suspend, revoke, expiry, atau termination harus segera menolak akses baru; histori dan dokumen tetap auditable.

STEP 18 — INVARIANT IMPLEMENTASI

AreaInvariant yang harus divalidasi API
OrganizationTenant root; workspace tidak pernah menjadi tenant key.
WorkspaceOwner tepat satu organization; collaboration link hanya boleh dari owner organization yang sama.
TeamTeam.organizationId wajib; TeamMember.memberId wajib dari tenant sama; employee opsional juga dari tenant sama.
MemberAccess lifecycle terpisah dari Employee. Revoke/suspend berbasis permission dan audit trail.
External participantMember aktif tenant asal; tidak pernah menciptakan Member tenant host.
CollaborationMinimal dua party saat activation; owner party merepresentasikan ownerOrganizationId; granted scope tidak melampaui scope collaboration.
ApprovalActor approver memenuhi role/team role yang disyaratkan dan berasal dari party relevan.
BillingEntitlement menentukan capability/quota tenant; permission menentukan otoritas actor. Keduanya wajib.
MoneyBigInt adalah minor unit currency; jangan menerima float/total yang dihitung client.
Cross databaseScalar ID tervalidasi, outbox/inbox, idempotency key, projection, reconciliation; tidak ada FK/join/transaksi Prisma lintas database.

Retensi

active → archived setelah periode pembukuan → evaluasi retention/legal hold → peringatan dan export backup → purged oleh job yang scoped serta auditable jika policy mengizinkan. Archive bukan delete; payroll finalized, ledger stock, invoice, approval, dan audit evidence tidak boleh dihapus hanya karena akses Member dicabut.

Kesimpulan batas domain

Organization     tenant root dan sumber tenant-scoping
Workspace        unit kerja opsional di bawah Organization
Member           akses organisasi dan RBAC
WorkspaceParticipant assignment banyak-ke-banyak ke unit kerja; participant eksternal tetap member tenant asalnya
Employee         lifecycle ketenagakerjaan
Category         konfigurasi pengalaman/modul bisnis, bukan otorisasi

Kesimpulan paling penting

Sekarang coba lihat:


USER

hanya:

identity
authentication

MEMBER

hanya:

Organization access
RBAC
permissions

EMPLOYEE

hanya:

employment
payroll
attendance
HR

POSITION

hanya:

organizational hierarchy

PERMISSION ROLE

hanya:

access control

PAYROLL

hanya:

immutable salary snapshot

INVENTORY MOVEMENT

hanya:

stock ledger

Dan inilah inti dari enterprise architecture

Bukan:

membuat banyak table

tetapi:

memisahkan lifecycle dan responsibility tiap domain

Healthy Architecture

Database Runner Schema

User
ā”œā”€ā”€ identitas/autentikasi layer
ā”œā”€ā”€ dapat memiliki banyak Organization
└── dapat tergabung di banyak Organization
 
Organization
ā”œā”€ā”€ memiliki banyak Member
ā”œā”€ā”€ dapat memiliki banyak Team
ā”œā”€ā”€ dapat memiliki banyak Workspace
ā”œā”€ā”€ dapat memiliki banyak Project
└── menjadi owner Collaboration yang dibuat dalam tenant-nya
 
Member // "siapa yang boleh mengakses Organisasi ini?"
ā”œā”€ā”€ kontrol akses
ā”œā”€ā”€ milik Pengguna
ā”œā”€ā”€ milik Organization
└── memiliki permission role
 
Employee // "siapa yang bekerja untuk organisasi ini?"
ā”œā”€ā”€ berasal dari Member (member bisa jadi employee, tapi employee harus member)
└── tenaga kerja / SDM (profil)
 
Team // "kumpulan member dan/atau employee"
ā”œā”€ā”€ organizationId wajib; tidak pernah lintas tenant
ā”œā”€ā”€ TeamMember.memberId wajib dan employeeId opsional
ā”œā”€ā”€ dapat dikaitkan ke Workspace, tetapi bukan milik Workspace
└── kumpulan tenaga kerja / SDM internal yang stabil
 
Project // "objek kerja dan deliverable"
ā”œā”€ā”€ target, timeline, budget, dan hasil
└── dapat memiliki beberapa Collaboration dan Workspace
 
Collaboration // "legal basis, parties, approval, dan scope bekerja bersama"
ā”œā”€ā”€ dapat bertipe internal_team, inter_organization, external_partner, event_committee, atau project_delivery
ā”œā”€ā”€ memiliki dua atau lebih CollaborationParty; party organisasi/team menyimpan role, status, scope, dan masa berlaku
ā”œā”€ā”€ menyimpan approval dan dokumen legal secara auditable
└── dapat mengatur beberapa Workspace milik organization owner
 
Workspace // "ruang kerja operasional untuk agenda"
ā”œā”€ā”€ organizationId wajib; hanya satu Organization yang menjadi owner sah
ā”œā”€ā”€ mengumpulkan participant internal lintas Team untuk sebuah agenda
ā”œā”€ā”€ participant eksternal berasal dari Organization asalnya melalui CollaborationParty aktif dan scope terbatas
└── tidak menjadikan participant eksternal sebagai Member Organization owner

Misalnya:

  • Pemilik Organisasi:

    • Bisa login
    • Bisa melihat dashboard
    • Bisa melihat revenue
    • Tetapi:
      • Bukan employee
      • Tidak menerima payroll
      • Tidak punya absensi
    • Maka:
      • Member = ada
      • Employee = tidak ada
    • Pemilik Organisasi bisa jadi member / karyawan di organisasi lain
    • Maka:
      • Member = ada (dalam organisasi lain tersebut)
      • Employee = ada (dalam organisasi lain tersebut)
  • Auditor eksternal:

    • boleh melihat laporan
    • bukan karyawan
    • Maka:
      • Member = ada
      • Employee = tidak ada
  • Karyawan resign:

    • Kita TIDAK BOLEH menghapus:
      • payroll[1]
      • attendance[1]
      • histori kerja[1]
    • Akses login dan Member dievaluasi lalu dicabut berdasarkan permission serta kebijakan HR, bukan melalui jalur operasional developer.
    • Maka:
      • Member[2] = nonaktif / revoked hak akses sesuai permission dan audit trail
      • Employee[2] = tetap ada agar hubungan kerja serta histori pembukuan kredibel
      • User[2] = tetap ada sebagai identitas digital semi-permanen

[1] Data aktif tersedia selama periode pembukuan. Setelahnya sistem mengarsipkan data; purge permanen hanya setelah retention policy/legal terpenuhi dan organization telah diperingatkan serta diberi kesempatan mengunduh backup. [2] Perubahan akses tidak menghapus history secara langsung. Retensi dan purge mengikuti kebijakan yang sama, legal hold, dan audit trail.


ideal:

  • auth lifecycle terpisah
  • HR lifecycle terpisah
  • aman
  • fleksibel
  • enterprise-ready

scalable:

  • login lifecycle ≠ employment lifecycle
  • access control ≠ HR lifecycle
  • employee resign ≠ account deleted

fokus User:

  • login
  • session
  • auth
  • credential
  • MFA
  • OAuth