Cyber Hybrid Hub API

Postgres-backed profile API for the Flutter app.

Setup

  1. Create the database (once):

    createdb cyberhybridhub
    
  2. Copy and edit environment variables:

    cp .env.example .env
    
  3. Install dependencies and run (from this server/ directory):

    dart pub get
    dart run bin/server.dart
    

The API listens on http://localhost:3000 by default (PORT in .env).

Endpoints

Method Path Auth
GET /v1/me/profile Authorization: Bearer <Firebase ID token>
PUT /v1/me/profile same
POST /v1/me/incoming-question same — pushes a question to the client via SignalR
POST /v1/me/questions/bootstrap ensure starter question at login
GET /v1/me/questions list unanswered questions (queue order)
POST /v1/me/questions/{id}/answer submit answer ({"answer": 0} default)
POST /v1/me/questions/{id}/defer move question to end of queue

SignalR — incoming questions

Hub URL: http://localhost:3000/hubs/questions

The Flutter app calls POST /v1/me/questions/bootstrap once at login to ensure a starter question exists (random correct answer from -10 to 10) when the user has none. After sign-in it connects to SignalR and listens for ReceiveQuestion. On each new WebSocket connection the API only delivers an existing unanswered question — it does not create new rows.

Client payload (correct answer is not sent):

{
  "id": "uuid",
  "assignedUserId": "firebase-uid",
  "text": "...",
  "sentAt": "...",
  "unansweredCount": 2
}

unansweredCount is the number of unanswered rows for that user (shown in the app when greater than 1).

questions table: id (UUID), assigned_user_id, question_text, user_response (nullable), correct_answer, created_at, modified_at.

Background question pipeline

A background worker runs inside the API process (enabled by default). On each interval it walks registered users, fetches data from public web APIs, and enqueues pipeline questions when the user's queue is not full.

Env var Default Purpose
QUESTION_WORKER_ENABLED true Set to false to disable the worker
QUESTION_WORKER_INTERVAL_SECONDS 60 Seconds between maintenance cycles
QUESTION_PIPELINE_TEST_MODE false Use random -10..10 starter-style questions instead of API copy

External APIs used

Branching flow

  1. Track choice — user swipes toward +10 (weather) or -10 (geography).
  2. Geography — yes/no population threshold, then capital confirmation; wrong population guess triggers a recovery question.
  3. Weather — yes/no warm/cool for a random European city, then a follow-up to continue weather or switch to geography.

When a user submits an answer (POST .../answer), the pipeline evaluates the response and may immediately create the next branched question and push it over SignalR.

Pipeline state is stored in user_pipeline_state; questions may include source_tag, pipeline_key, and pipeline_step columns (migration 003_question_pipeline.sql).

Test from the shell (replace ID_TOKEN):

curl -s -X POST http://localhost:3000/v1/me/incoming-question \
  -H "Authorization: Bearer ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"What is your preferred contact method?"}'

Flutter client

Run the app with the API URL (defaults to http://localhost:3000):

flutter run --dart-define=API_BASE_URL=http://localhost:3000