Files
halobestie-clone/requirement/phase1-api-contract.md
ramadhan sjamsani a7a2a32d27 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>
2026-04-05 10:08:42 +08:00

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 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:

{
  "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