Phase 1 scaffold: auth for all apps

- Backend: Fastify with two listeners (public + internal), routes, services, DB migration + seed
- client_app: Flutter with BLoC, all auth screens (welcome, display name, register, OTP, force-register)
- mitra_app: Flutter with BLoC, OTP-only login
- control_center: React + Vite, email/password login, mitra/user management, anonymity settings
- Docs: phase1 plan, API contract, client app mockup
- CLAUDE.md and shared memory for all subprojects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 10:08:42 +08:00
commit a7a2a32d27
85 changed files with 3953 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
# Memory Index
- [Requirement Context](context.md) — Phased docs, naming conventions, real-time/chat deferred to future phase

View File

@@ -0,0 +1,13 @@
---
name: Requirement Context
description: Conventions and scope for Halo Bestie requirement documents
type: project
---
Phased requirement documents for all apps.
**Naming convention:** `phase<N>-<domain>.md` (e.g. `phase1-auth.md`, `phase1-payment.md`)
**Each doc must state:** which app(s) it affects (`client_app`, `mitra_app`, `control_center`, `backend`) and any backend route implications.
**Known deferred:** Real-time chat and live features are out of scope for initial phases — planned for a future phase.

17
requirement/CLAUDE.md Normal file
View File

@@ -0,0 +1,17 @@
# Halo Bestie — Requirements
This folder contains requirement documents for all project phases.
> See root `CLAUDE.md` for full project context and architectural decisions.
## Purpose
- Document feature requirements per phase
- Define acceptance criteria
- Capture domain rules and business logic decisions
## Conventions
- Name documents clearly by phase and domain (e.g. `phase1-auth.md`, `phase1-payment.md`)
- Each document should reference which app(s) it affects: `client_app`, `mitra_app`, `control_center`, `backend`
- Real-time / chat features are planned for a future phase

View File

@@ -0,0 +1,511 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Client App Mockup — Halo Bestie</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #f0f0f0;
padding: 40px 20px;
}
h1 {
text-align: center;
margin-bottom: 8px;
color: #333;
}
.subtitle {
text-align: center;
color: #888;
margin-bottom: 40px;
font-size: 14px;
}
.screens {
display: flex;
flex-wrap: wrap;
gap: 32px;
justify-content: center;
}
.screen-label {
text-align: center;
font-size: 13px;
font-weight: 600;
color: #555;
margin-bottom: 10px;
}
/* Android phone frame */
.phone {
width: 320px;
height: 640px;
background: #1a1a1a;
border-radius: 40px;
padding: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
position: relative;
}
.phone::before {
content: '';
display: block;
width: 80px;
height: 6px;
background: #333;
border-radius: 3px;
margin: 0 auto 8px;
}
.phone::after {
content: '';
display: block;
width: 40px;
height: 6px;
background: #333;
border-radius: 3px;
margin: 8px auto 0;
}
.screen {
width: 100%;
height: 560px;
background: #fff;
border-radius: 28px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Status bar */
.status-bar {
background: #fff;
padding: 6px 16px;
display: flex;
justify-content: space-between;
font-size: 10px;
color: #333;
font-weight: 600;
}
/* App bar */
.app-bar {
background: #fff;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid #f0f0f0;
}
.app-bar .back-btn {
font-size: 18px;
color: #333;
cursor: pointer;
}
.app-bar .title {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
/* Content area */
.content {
flex: 1;
padding: 24px 20px;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Colors */
:root {
--primary: #6C63FF;
--primary-light: #EEF0FF;
--text: #1a1a1a;
--text-light: #888;
--border: #e0e0e0;
--surface: #f8f8f8;
}
/* Components */
.btn-primary {
background: var(--primary);
color: white;
border: none;
border-radius: 12px;
padding: 14px;
font-size: 15px;
font-weight: 600;
width: 100%;
cursor: pointer;
text-align: center;
}
.btn-outline {
background: white;
color: var(--primary);
border: 1.5px solid var(--primary);
border-radius: 12px;
padding: 14px;
font-size: 15px;
font-weight: 600;
width: 100%;
cursor: pointer;
text-align: center;
}
.btn-social {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
border-radius: 12px;
padding: 13px;
font-size: 14px;
font-weight: 500;
width: 100%;
cursor: pointer;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.input-field {
width: 100%;
border: 1.5px solid var(--border);
border-radius: 12px;
padding: 13px 14px;
font-size: 14px;
color: var(--text);
outline: none;
}
.input-label {
font-size: 12px;
font-weight: 600;
color: var(--text-light);
margin-bottom: 6px;
}
.input-group {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
.divider-or {
display: flex;
align-items: center;
gap: 10px;
margin: 16px 0;
color: var(--text-light);
font-size: 12px;
}
.divider-or::before, .divider-or::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.big-title {
font-size: 26px;
font-weight: 800;
color: var(--text);
margin-bottom: 6px;
}
.big-subtitle {
font-size: 14px;
color: var(--text-light);
margin-bottom: 36px;
}
.hero-icon {
font-size: 56px;
text-align: center;
margin-bottom: 16px;
}
.tag {
display: inline-block;
background: var(--primary-light);
color: var(--primary);
border-radius: 20px;
padding: 4px 12px;
font-size: 11px;
font-weight: 600;
margin-bottom: 12px;
}
.otp-boxes {
display: flex;
gap: 8px;
justify-content: center;
margin: 20px 0;
}
.otp-box {
width: 42px;
height: 50px;
border: 2px solid var(--border);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 700;
color: var(--primary);
}
.otp-box.active {
border-color: var(--primary);
}
.helper-text {
font-size: 12px;
color: var(--text-light);
text-align: center;
margin-top: 8px;
}
.banner {
background: #FFF3E0;
border: 1px solid #FFB74D;
border-radius: 12px;
padding: 14px;
font-size: 13px;
color: #E65100;
margin-bottom: 20px;
line-height: 1.5;
}
.home-greeting {
font-size: 22px;
font-weight: 700;
color: var(--text);
margin-bottom: 4px;
}
.home-sub {
font-size: 13px;
color: var(--text-light);
margin-bottom: 28px;
}
.home-card {
background: var(--primary-light);
border-radius: 16px;
padding: 20px;
text-align: center;
color: var(--primary);
font-size: 13px;
font-weight: 500;
}
.home-card .icon { font-size: 32px; margin-bottom: 8px; }
.bottom-nav {
display: flex;
border-top: 1px solid var(--border);
padding: 10px 0 6px;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
font-size: 10px;
color: var(--text-light);
}
.nav-item.active { color: var(--primary); }
.nav-item .icon { font-size: 20px; }
</style>
</head>
<body>
<h1>Halo Bestie — Client App</h1>
<p class="subtitle">Phase 1 · Android Screen Mockups</p>
<div class="screens">
<!-- 1. Welcome -->
<div>
<div class="screen-label">1. Welcome</div>
<div class="phone">
<div class="screen">
<div class="status-bar"><span>9:41</span><span>● ● ▲</span></div>
<div class="content" style="justify-content: center; align-items: center; text-align: center;">
<div class="hero-icon">💬</div>
<div class="big-title">Halo Bestie</div>
<div class="big-subtitle">Tempat curhat kamu</div>
<div style="width: 100%; margin-top: 8px; display: flex; flex-direction: column; gap: 12px;">
<div class="btn-primary">Lanjut sebagai Tamu</div>
<div class="btn-outline">Daftar / Masuk</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. Pick Display Name -->
<div>
<div class="screen-label">2. Pick Display Name</div>
<div class="phone">
<div class="screen">
<div class="status-bar"><span>9:41</span><span>● ● ▲</span></div>
<div class="app-bar">
<span class="back-btn"></span>
<span class="title">Siapa namamu?</span>
</div>
<div class="content">
<div class="tag">Tamu</div>
<div style="font-size: 20px; font-weight: 700; color: var(--text); margin-bottom: 8px;">Pilih nama panggilanmu</div>
<div style="font-size: 13px; color: var(--text-light); margin-bottom: 28px; line-height: 1.6;">
Nama ini tidak akan terlihat oleh siapapun selain mitra kamu. Kamu bisa pakai nama samaran.
</div>
<div class="input-group">
<div class="input-label">NAMA PANGGILAN</div>
<input class="input-field" placeholder="contoh: Angin Malam" value="Angin Malam" />
</div>
<div style="margin-top: auto;">
<div class="btn-primary">Lanjut →</div>
</div>
</div>
</div>
</div>
</div>
<!-- 3. Register -->
<div>
<div class="screen-label">3. Daftar / Masuk</div>
<div class="phone">
<div class="screen">
<div class="status-bar"><span>9:41</span><span>● ● ▲</span></div>
<div class="app-bar">
<span class="back-btn"></span>
<span class="title">Masuk / Daftar</span>
</div>
<div class="content">
<div style="font-size: 20px; font-weight: 700; color: var(--text); margin-bottom: 6px;">Selamat datang</div>
<div style="font-size: 13px; color: var(--text-light); margin-bottom: 24px;">Masuk atau buat akun baru</div>
<div class="btn-social" style="margin-bottom: 10px;">
<span>🔵</span> Lanjut dengan Google
</div>
<div class="btn-social">
<span>🍎</span> Lanjut dengan Apple
</div>
<div class="divider-or">atau</div>
<div class="input-group">
<div class="input-label">NOMOR HP</div>
<input class="input-field" placeholder="+628xxxxxxxxxx" />
</div>
<div class="btn-primary">Kirim OTP</div>
</div>
</div>
</div>
</div>
<!-- 4. OTP -->
<div>
<div class="screen-label">4. Verifikasi OTP</div>
<div class="phone">
<div class="screen">
<div class="status-bar"><span>9:41</span><span>● ● ▲</span></div>
<div class="app-bar">
<span class="back-btn"></span>
<span class="title">Masukkan OTP</span>
</div>
<div class="content">
<div style="font-size: 20px; font-weight: 700; color: var(--text); margin-bottom: 8px;">Cek SMS kamu</div>
<div style="font-size: 13px; color: var(--text-light); margin-bottom: 28px; line-height: 1.6;">
Kode OTP telah dikirim ke<br/><strong style="color: var(--text);">+628123456789</strong>
</div>
<div class="otp-boxes">
<div class="otp-box">3</div>
<div class="otp-box">8</div>
<div class="otp-box">4</div>
<div class="otp-box active"></div>
<div class="otp-box"></div>
<div class="otp-box"></div>
</div>
<div class="helper-text">Kirim ulang dalam 00:47</div>
<div style="margin-top: auto;">
<div class="btn-primary">Verifikasi</div>
</div>
</div>
</div>
</div>
</div>
<!-- 5. Force Register Wall -->
<div>
<div class="screen-label">5. Force Register Wall</div>
<div class="phone">
<div class="screen">
<div class="status-bar"><span>9:41</span><span>● ● ▲</span></div>
<div class="app-bar">
<span class="title">Verifikasi Akun</span>
</div>
<div class="content">
<div class="banner">
⚠️ Untuk melanjutkan, kamu perlu mendaftarkan akun. Ini hanya memakan waktu sebentar.
</div>
<div style="font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 16px;">Pilih cara daftar</div>
<div class="btn-social" style="margin-bottom: 10px;">
<span>🔵</span> Lanjut dengan Google
</div>
<div class="btn-social">
<span>🍎</span> Lanjut dengan Apple
</div>
<div class="divider-or">atau</div>
<div class="input-group">
<div class="input-label">NOMOR HP</div>
<input class="input-field" placeholder="+628xxxxxxxxxx" />
</div>
<div class="btn-primary">Kirim OTP</div>
</div>
</div>
</div>
</div>
<!-- 6. Home (Phase 1 placeholder) -->
<div>
<div class="screen-label">6. Home (placeholder)</div>
<div class="phone">
<div class="screen">
<div class="status-bar"><span>9:41</span><span>● ● ▲</span></div>
<div class="app-bar">
<span class="title" style="flex:1;">Halo Bestie</span>
<span style="font-size: 20px; color: var(--text-light);">🔔</span>
</div>
<div class="content">
<div class="home-greeting">Halo, Angin Malam 👋</div>
<div class="home-sub">Semoga harimu menyenangkan</div>
<div class="home-card">
<div class="icon">💜</div>
<div style="font-size: 15px; font-weight: 700; color: var(--primary); margin-bottom: 6px;">Fitur segera hadir</div>
<div>Sesi curhat dengan mitra profesional akan tersedia di Phase 2.</div>
</div>
</div>
<div class="bottom-nav">
<div class="nav-item active"><span class="icon">🏠</span>Beranda</div>
<div class="nav-item"><span class="icon">💬</span>Sesi</div>
<div class="nav-item"><span class="icon">👤</span>Profil</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,452 @@
# Phase 1 — API Contract
## General Conventions
- Base URL public: `https://api.halobestie.com`
- Base URL internal: `https://internal.halobestie.com`
- All requests: `Content-Type: application/json`
- All authenticated requests: `Authorization: Bearer <firebase_jwt>`
- All responses follow this envelope:
```json
// Success
{
"success": true,
"data": { ... }
}
// Error
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human readable message"
}
}
```
---
## Error Codes
| Code | HTTP | Description |
|---|---|---|
| `UNAUTHORIZED` | 401 | Missing or invalid Firebase JWT |
| `FORBIDDEN` | 403 | Valid token but insufficient role/permission |
| `NOT_FOUND` | 404 | Resource not found |
| `VALIDATION_ERROR` | 422 | Request body/params failed validation |
| `ACCOUNT_NOT_FOUND` | 404 | Phone number not registered as mitra |
| `ACCOUNT_INACTIVE` | 403 | Mitra account is inactive |
| `DISPLAY_NAME_REQUIRED` | 422 | Display name not provided |
| `ALREADY_REGISTERED` | 409 | Anonymous user already has phone/social linked |
| `INTERNAL_ERROR` | 500 | Unexpected server error |
---
## Public API (port 3000)
### Customer
#### `POST /api/shared/customer/anonymous`
Create an anonymous customer with a display name.
**Auth:** None
**Request:**
```json
{
"display_name": "Angin Malam"
}
```
**Response `201`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"display_name": "Angin Malam",
"is_anonymous": true,
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `DISPLAY_NAME_REQUIRED`, `VALIDATION_ERROR`
---
#### `POST /api/shared/customer/link`
Link phone OTP or social login to an existing anonymous customer.
**Auth:** Firebase JWT (anonymous customer)
**Request:**
```json
{
"customer_id": "uuid",
"firebase_uid": "firebase_uid_from_auth"
}
```
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"display_name": "Angin Malam",
"is_anonymous": false,
"phone": "+628xxxxxxxxxx",
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `NOT_FOUND`, `ALREADY_REGISTERED`
---
#### `POST /api/client/auth/verify`
Verify Firebase JWT and return customer profile. Called after every login.
**Auth:** Firebase JWT (customer)
**Request:** _(empty body)_
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"display_name": "Angin Malam",
"is_anonymous": false,
"phone": "+628xxxxxxxxxx",
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `NOT_FOUND`
---
### Mitra
#### `POST /api/mitra/auth/verify`
Verify Firebase JWT and return mitra profile. Called after OTP login.
**Auth:** Firebase JWT (mitra)
**Request:** _(empty body)_
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"display_name": "Dr. Budi",
"phone": "+628xxxxxxxxxx",
"is_active": true,
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `ACCOUNT_NOT_FOUND`, `ACCOUNT_INACTIVE`
---
### Anonymity Config (read-only for apps)
#### `GET /api/shared/config/anonymity`
Get current anonymity setting. Apps poll this to decide whether to show force-register wall.
**Auth:** Firebase JWT (customer)
**Response `200`:**
```json
{
"success": true,
"data": {
"anonymity_enabled": true
}
}
```
---
## Internal API (port 3001)
> All internal routes require a valid Firebase JWT with `role` verified server-side.
### Auth
#### `POST /internal/auth/verify`
Verify Firebase JWT and return control center user profile with role and permissions.
**Auth:** Firebase JWT (control center user)
**Request:** _(empty body)_
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"email": "admin@halobestie.com",
"display_name": "Admin",
"role": {
"id": "uuid",
"name": "super_admin",
"permissions": {
"mitra": ["create", "read", "update", "delete"],
"control_center_users": ["create", "read", "update", "delete"],
"config": ["read", "update"]
}
}
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`
---
### Mitra Management
#### `POST /internal/mitras`
Create a new mitra record.
**Auth:** Firebase JWT (control center user, requires `mitra:create` permission)
**Request:**
```json
{
"phone": "+628xxxxxxxxxx",
"display_name": "Dr. Budi"
}
```
**Response `201`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"phone": "+628xxxxxxxxxx",
"display_name": "Dr. Budi",
"is_active": false,
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`
---
#### `GET /internal/mitras`
List all mitras.
**Auth:** Firebase JWT (control center user, requires `mitra:read` permission)
**Query params:**
- `page` (default: 1)
- `limit` (default: 20)
- `is_active` (optional: `true` | `false`)
**Response `200`:**
```json
{
"success": true,
"data": {
"items": [
{
"id": "uuid",
"phone": "+628xxxxxxxxxx",
"display_name": "Dr. Budi",
"is_active": true,
"created_at": "2026-04-04T00:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
}
```
---
#### `PATCH /internal/mitras/:id/status`
Activate or deactivate a mitra.
**Auth:** Firebase JWT (control center user, requires `mitra:update` permission)
**Request:**
```json
{
"is_active": true
}
```
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"is_active": true
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `VALIDATION_ERROR`
---
### Control Center User Management
#### `POST /internal/control-center-users`
Create a new control center user.
**Auth:** Firebase JWT (requires `control_center_users:create` permission)
**Request:**
```json
{
"email": "operator@halobestie.com",
"display_name": "Operator 1",
"role_id": "uuid"
}
```
**Response `201`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"email": "operator@halobestie.com",
"display_name": "Operator 1",
"role": {
"id": "uuid",
"name": "operator"
},
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`
---
#### `GET /internal/control-center-users`
List all control center users.
**Auth:** Firebase JWT (requires `control_center_users:read` permission)
**Query params:**
- `page` (default: 1)
- `limit` (default: 20)
**Response `200`:**
```json
{
"success": true,
"data": {
"items": [
{
"id": "uuid",
"email": "operator@halobestie.com",
"display_name": "Operator 1",
"role": {
"id": "uuid",
"name": "operator"
},
"created_at": "2026-04-04T00:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
}
```
---
### Roles
#### `GET /internal/roles`
List all roles.
**Auth:** Firebase JWT (requires `control_center_users:read` permission)
**Response `200`:**
```json
{
"success": true,
"data": [
{
"id": "uuid",
"name": "super_admin",
"permissions": {
"mitra": ["create", "read", "update", "delete"],
"control_center_users": ["create", "read", "update", "delete"],
"config": ["read", "update"]
}
}
]
}
```
---
### Config
#### `GET /internal/config/anonymity`
Get anonymity setting.
**Auth:** Firebase JWT (requires `config:read` permission)
**Response `200`:**
```json
{
"success": true,
"data": {
"anonymity_enabled": true
}
}
```
---
#### `PATCH /internal/config/anonymity`
Toggle anonymity setting.
**Auth:** Firebase JWT (requires `config:update` permission)
**Request:**
```json
{
"anonymity_enabled": false
}
```
**Response `200`:**
```json
{
"success": true,
"data": {
"anonymity_enabled": false
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`

119
requirement/phase1-plan.md Normal file
View File

@@ -0,0 +1,119 @@
# Phase 1 Plan — Authentication
## Overview
Three separate auth flows across three apps, backed by one Fastify backend, Firebase Auth, and PostgreSQL.
---
## Database Schema
### `customers`
| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `firebase_uid` | VARCHAR | null if anonymous |
| `phone` | VARCHAR | null if anonymous |
| `display_name` | VARCHAR | user-chosen, never from social |
| `is_anonymous` | BOOLEAN | true until phone/social linked |
| `created_at` | TIMESTAMP | |
### `mitras`
| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `firebase_uid` | VARCHAR | set on first login |
| `phone` | VARCHAR | primary identifier |
| `display_name` | VARCHAR | |
| `is_active` | BOOLEAN | toggled by control center |
| `created_at` | TIMESTAMP | |
### `control_center_users`
| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `firebase_uid` | VARCHAR | |
| `email` | VARCHAR | |
| `display_name` | VARCHAR | |
| `role_id` | FK → `roles` | |
| `created_at` | TIMESTAMP | |
### `roles`
| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `name` | VARCHAR | e.g. `super_admin`, `operator` |
| `permissions` | JSONB | flexible permissions object |
| `created_at` | TIMESTAMP | |
---
## Backend (`/backend`)
### Public routes (port 3000)
- `POST /api/shared/customer/anonymous` — create anonymous customer with display name
- `POST /api/shared/customer/link` — link phone/social to existing anonymous customer
- `POST /api/client/auth/verify` — verify Firebase JWT, return customer profile
- `POST /api/mitra/auth/verify` — verify Firebase JWT, return mitra profile
### Internal routes (port 3001)
- `POST /internal/mitras` — create mitra record
- `PATCH /internal/mitras/:id/status` — activate/deactivate mitra
- `POST /internal/control-center-users` — create control center user
- `GET /internal/control-center-users` — list users
- `POST /internal/auth/verify` — verify Firebase JWT, return CC user + role + permissions
- `GET /internal/config/anonymity` — get anonymity setting
- `PATCH /internal/config/anonymity` — toggle anonymity on/off
---
## client_app (`/client_app`)
**Screens:**
1. **Welcome** — "Continue as Guest" or "Register"
2. **Pick Display Name** — shown to all users (anonymous and registering)
3. **Register** — phone OTP or social login (Google/Apple)
4. **Force Register Wall** — shown after session ends if anonymity is disabled; display name pre-filled
**Firebase Auth flows:**
- Phone OTP via `firebase_auth`
- Google Sign-In via `google_sign_in`
- Apple Sign-In via `sign_in_with_apple`
---
## mitra_app (`/mitra_app`)
**Screens:**
1. **Login** — phone number input
2. **OTP Verification**
3. **Home** (post-login, Phase 1 placeholder)
**Notes:**
- No self-register screen — login only
- If phone not found in `mitras` table → show error "Account not found. Contact your administrator."
- If mitra `is_active = false` → show error "Account is inactive. Contact your administrator."
---
## control_center (`/control_center`)
**Screens:**
1. **Login** — email + password (Firebase Auth)
2. **Mitra Management** — create mitra, toggle active/inactive
3. **Control Center User Management** — create users, assign roles
4. **Settings** — toggle anonymity on/off
---
## Seed Script
- Creates first `super_admin` role with full permissions
- Creates first control center user (email + password via Firebase Auth + DB record)
---
## Out of Scope for Phase 1
- Mitra onboarding flow (documents, verification)
- Chat / session features
- Payment / trial period
- Real-time features
- Specific role definitions (RBAC scaffolded, roles defined later)

27
requirement/phase1.md Normal file
View File

@@ -0,0 +1,27 @@
# Context for Halo-Bestie Chat App
We are building chat application to help our user to share their feelings (Indonesian called it "curhat") to trainned professional. Our users will be called Customer, while our professional will be called Mitra. This is a paid service with trial period only available when user registering. The service is duration based paid service, configurable through control center. Trial period is configurable via control period, for both duration and availability.
## Phase 1
We build application for both mitra (our professional), and customer for them to communicate via chat. Control center also need to be build to manage communication (re routing, seeing whose live, etc), manage application and managing features. But on this phase, we want to start from authentication only for both Mitra, Customer and Control Center user. The functionality needed for this phase:
1. Customer can do self register or go as anonymous
2. Both anonymous and registered user can decide who they want to be called. If social login is used, system NEVER used their social name
3. Anonymity of the user can be enabled or disabled through control center
4. When the anonymity is set to disable, anonymous user will be forced to register by either linking to their social login or through phone number OTP
5. Customer registration can be done through mobile number with OTP or social login.
6. Mitra is a user that registered through control center. They wont be able to do self register
7. Mitra primary identification is phone number and logged on through OTP
8. Note that Mitra and Customer can have same number. Because mitra is also human and need to vent their problem as wel :)
9. Control center user is managed by control center admin
10. Mitra on boarding flow, is not yet covered in app. So it is a matter of creating Mitra's data, and set it to active or inactive
## Tech Stack
- mitra app -> flutter for both ios and android
- client_app -> flutter for both ios and android
- backend -> fastify
- payment gateway -> xendit
- auth -> firebase auth
- database -> postgresql

50
requirement/scracthpad.md Normal file
View File

@@ -0,0 +1,50 @@
# Context for Halo-Bestie Chat App
We are building chat application to help our user to share their feelings (Indonesian called it "curhat") to trainned professional. Our users will be called Customer, while our professional will be called Mitra. This is a paid service with trial period only available when user registering. The service is duration based paid service, configurable through control center. Trial period is configurable via control period, for both duration and availability.
## Phase 1
We build application for both mitra (our professional), and customer for them to communicate via chat. Control center also need to be build to manage communication (re routing, seeing whose live, etc), manage application and managing features. It should provide following functionality:
1. Customer can do self register or go as anonymous. But only registered user that never do any transaction can have trial period
2. Anonymity of the user can be enabled or disabled through control center
3. When the anonymity is set to disable, anonymous user will be forced to register by either linking to their social login or through phone number OTP
4. Customer registration can be done through mobile number with OTP or social login.
5. Mitra is a user that registered through control center. They wont be able to do self register
6. Mitra primary identification is phone number and logged on through OTP
7. Note that Mitra and Customer can have same number. Because mitra is also human and need to vent as well.
8. Control center user is managed by control center admin
9. Mitra on boarding flow, is not yet covered in app. So it is a matter of creating Mitra's data, and set it to active or inactive
10. Curhat session is a service that will be bound by time
11. The timing is prepaid, that is sold per x minute based on configuration in control center
12.
### Session Booking Flow
1. Customer click CTA for mulai curhat
2. System will look for available Mitra, and blast them that there is Customer waiting
3. When one of mitra confirm, system will pair Mitra with Customer. Only one Mitra can be paired with Customer
4. Check for trial eligibility
1. If allowed for trial, system will go for next process
2. If not then:
1. system will trigger package selection for payment
2. Customer will select package, and do CTA for payment
3. Once payment confirmed, system will go for next process
5. Curhat session started. Timing must be done in back end
6. When session is over (package time is over) there are 3 cases:
1. Customer want to extend, and mitra is confirmed the extension:
1. User will be triggered for another package selection and payment
2. Customer want to extend, but Mitra is rejecting:
* System will trigger session ending
3. Customer decide to end the session, system will trigger session ending
7. Session ending will request both Mitra and Customer closing message. This message will be stored as closing message from both
## Tech Stack
- mitra app -> flutter for both ios and android
- client_app -> flutter for both ios and android
- backend -> fastify
- payment gateway -> xendit
- auth -> firebase auth
- database -> postgresql