- 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>
7.8 KiB
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:
// 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:
{
"display_name": "Angin Malam"
}
Response 201:
{
"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:
{
"customer_id": "uuid",
"firebase_uid": "firebase_uid_from_auth"
}
Response 200:
{
"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:
{
"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:
{
"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:
{
"success": true,
"data": {
"anonymity_enabled": true
}
}
Internal API (port 3001)
All internal routes require a valid Firebase JWT with
roleverified 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:
{
"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:
{
"phone": "+628xxxxxxxxxx",
"display_name": "Dr. Budi"
}
Response 201:
{
"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:
{
"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:
{
"is_active": true
}
Response 200:
{
"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:
{
"email": "operator@halobestie.com",
"display_name": "Operator 1",
"role_id": "uuid"
}
Response 201:
{
"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:
{
"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:
{
"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:
{
"success": true,
"data": {
"anonymity_enabled": true
}
}
PATCH /internal/config/anonymity
Toggle anonymity setting.
Auth: Firebase JWT (requires config:update permission)
Request:
{
"anonymity_enabled": false
}
Response 200:
{
"success": true,
"data": {
"anonymity_enabled": false
}
}
Errors: UNAUTHORIZED, FORBIDDEN, VALIDATION_ERROR